Custom Exceptions
Understand how to create and use custom exception types in Dart.
What is it?
Custom exceptions are user-defined exception classes that extend or implement the Exception class. They allow you to create domain-specific error types with rich context, enabling precise error handling and better debugging.
Why does it exist?
Custom exceptions exist to:
- Create domain-specific error types
- Provide rich error context
- Enable precise error handling
- Improve code readability
- Add custom behavior to errors
- Simplify debugging with structured error data
Basic Custom Exceptions
Creating Custom Exceptions
Custom exceptions are created by extending or implementing the
Exceptionclass. They can include additional fields and methods.
// Simple custom exception
class MyException implements Exception {
final String message;
MyException(this.message);
@override
String toString() => 'MyException: $message';
}
// Exception with multiple fields
class ValidationException implements Exception {
final String field;
final String message;
final dynamic value;
ValidationException({
required this.field,
required this.message,
this.value,
});
@override
String toString() {
if (value != null) {
return 'ValidationException: $field - $message (value: $value)';
}
return 'ValidationException: $field - $message';
}
}
// Usage
void validateEmail(String email) {
if (email.isEmpty) {
throw MyException('Email cannot be empty');
}
if (!email.contains('@')) {
throw ValidationException(
field: 'email',
message: 'Invalid email format',
value: email,
);
}
}
void main() {
try {
validateEmail('invalid');
} catch (e) {
print('Caught: $e');
}
}
// Output:
// Caught: ValidationException: email - Invalid email format (value: invalid)
What's happening here? - Custom exceptions implement
Exception- They can have custom fields and methods -toString()provides meaningful error messages - Each exception type can carry specific data
Custom Exceptions with Methods
Custom exceptions can include methods and behaviors.
class NetworkException implements Exception {
final int statusCode;
final String message;
final String? url;
final DateTime timestamp;
NetworkException({
required this.statusCode,
required this.message,
this.url,
}) : timestamp = DateTime.now();
bool get isClientError => statusCode >= 400 && statusCode < 500;
bool get isServerError => statusCode >= 500 && statusCode < 600;
bool get isNotFound => statusCode == 404;
bool get isTimeout => statusCode == 408;
String get userFriendlyMessage {
if (isNotFound) return 'The requested resource was not found';
if (isTimeout) return 'The request timed out. Please try again';
if (isClientError) return 'An error occurred with your request';
if (isServerError) return 'The server encountered an error';
return 'A network error occurred';
}
@override
String toString() {
var base = 'NetworkException: $statusCode - $message';
if (url != null) base += ' (URL: $url)';
base += ' at $timestamp';
return base;
}
}
void fetchData(String url) {
if (url.contains('404')) {
throw NetworkException(
statusCode: 404,
message: 'Resource not found',
url: url,
);
}
if (url.contains('500')) {
throw NetworkException(
statusCode: 500,
message: 'Internal server error',
url: url,
);
}
print('Data fetched from $url');
}
void main() {
try {
fetchData('https://api.example.com/404');
} on NetworkException catch (e) {
print('Error: ${e.userFriendlyMessage}');
print('Status: ${e.statusCode}');
print('Is client error: ${e.isClientError}');
print('Timestamp: ${e.timestamp}');
}
}
// Output:
// Error: The requested resource was not found
// Status: 404
// Is client error: true
// Timestamp: 2024-01-01 12:00:00.000
Key insights: - Custom exceptions can have methods - They can provide user-friendly messages - You can add computed properties - Rich context helps with debugging - Different handling based on error type
Custom Exception Hierarchy
Creating Exception Hierarchies
You can create a hierarchy of custom exceptions for different error types.
// Base exception
class AppException implements Exception {
final String message;
final StackTrace? stackTrace;
AppException(this.message, [this.stackTrace]);
@override
String toString() => 'AppException: $message';
}
// Network-related exceptions
class NetworkException extends AppException {
NetworkException(String message) : super(message);
}
class TimeoutException extends NetworkException {
final int timeoutSeconds;
TimeoutException(this.timeoutSeconds)
: super('Connection timed out after ${timeoutSeconds}s');
}
class ConnectionException extends NetworkException {
final String host;
ConnectionException(this.host)
: super('Cannot connect to $host');
}
// Data-related exceptions
class DataException extends AppException {
final String data;
DataException(String message, this.data) : super(message);
}
class ValidationException extends DataException {
final String field;
ValidationException(String message, this.field, String data)
: super(message, data);
}
class ParsingException extends DataException {
ParsingException(String message, String data)
: super(message, data);
}
// Usage
void fetchUserData(String userId) {
if (userId.isEmpty) {
throw ValidationException(
'User ID cannot be empty',
'userId',
userId,
);
}
if (userId == 'timeout') {
throw TimeoutException(30);
}
if (userId == 'error') {
throw ConnectionException('api.example.com');
}
print('Fetched user: $userId');
}
void main() {
try {
fetchUserData('');
} on ValidationException catch (e) {
print('Validation error on ${e.field}: ${e.message}');
} on TimeoutException catch (e) {
print('Timeout after ${e.timeoutSeconds}s');
} on ConnectionException catch (e) {
print('Cannot connect to ${e.host}');
} on AppException catch (e) {
print('App error: ${e.message}');
}
try {
fetchUserData('timeout');
} on NetworkException catch (e) {
print('Network error: ${e.message}');
}
}
Real-World Example
Complete Custom Exception System
A complete exception system for a real-world application.
// Base exception with formatting
abstract class AppException implements Exception {
final String code;
final String message;
final String? details;
final DateTime timestamp;
AppException({
required this.code,
required this.message,
this.details,
}) : timestamp = DateTime.now();
String get formattedMessage {
var base = '[$code] $message';
if (details != null) base += '\nDetails: $details';
base += '\nTimestamp: $timestamp';
return base;
}
@override
String toString() => formattedMessage;
}
// Authentication exceptions
class AuthException extends AppException {
AuthException({required String code, required String message, String? details})
: super(code: 'AUTH_$code', message: message, details: details);
}
class InvalidCredentialsException extends AuthException {
InvalidCredentialsException({String? details})
: super(
code: 'INVALID_CREDENTIALS',
message: 'Invalid username or password',
details: details,
);
}
class SessionExpiredException extends AuthException {
SessionExpiredException()
: super(
code: 'SESSION_EXPIRED',
message: 'Your session has expired. Please log in again.',
);
}
// Data exceptions
class DataException extends AppException {
DataException({required String code, required String message, String? details})
: super(code: 'DATA_$code', message: message, details: details);
}
class NotFoundException extends DataException {
final String resource;
final String id;
NotFoundException(this.resource, this.id)
: super(
code: 'NOT_FOUND',
message: '$resource not found',
details: 'Resource: $resource, ID: $id',
);
}
class ConflictException extends DataException {
final String resource;
final String field;
final String value;
ConflictException(this.resource, this.field, this.value)
: super(
code: 'CONFLICT',
message: '$resource already exists',
details: 'Field: $field, Value: $value',
);
}
// Service layer
class UserService {
final Map<String, String> _users = {
'alice': 'alice@example.com',
'bob': 'bob@example.com',
};
String getUser(String username, String token) {
// Authentication
if (token.isEmpty) {
throw SessionExpiredException();
}
if (token != 'valid_token') {
throw InvalidCredentialsException(details: 'Invalid token provided');
}
// Data retrieval
if (!_users.containsKey(username)) {
throw NotFoundException('User', username);
}
return _users[username]!;
}
void createUser(String username, String email) {
if (_users.containsKey(username)) {
throw ConflictException('User', 'username', username);
}
_users[username] = email;
}
}
void main() {
var service = UserService();
// Test: Session expired
try {
var user = service.getUser('alice', '');
} on AuthException catch (e) {
print('Auth error: ${e.message}');
}
// Test: Invalid credentials
try {
var user = service.getUser('alice', 'invalid');
} on InvalidCredentialsException catch (e) {
print('Invalid credentials: ${e.details}');
}
// Test: User not found
try {
var user = service.getUser('charlie', 'valid_token');
} on NotFoundException catch (e) {
print('Not found: ${e.resource} ${e.id}');
}
// Test: User conflict
try {
service.createUser('alice', 'alice@example.com');
} on ConflictException catch (e) {
print('Conflict: ${e.field} ${e.value}');
}
}
// Output:
// Auth error: Your session has expired. Please log in again.
// Invalid credentials: Invalid token provided
// Not found: User charlie
// Conflict: username alice
Best Practices
Use Custom Exceptions for Specific Errors
// Good: Specific custom exceptions
class NetworkError extends Exception {
final int statusCode;
NetworkError(this.statusCode);
}
// Bad: Generic exception
throw Exception('Network error');
Provide Rich Context
// Good: Rich context in exception
class DataNotFoundException extends Exception {
final String resource;
final String id;
final String? additionalInfo;
DataNotFoundException(this.resource, this.id, {this.additionalInfo});
}
// Bad: No context
throw Exception('Data not found');
Common Mistakes
Not Implementing toString()
Wrong:
class CustomException implements Exception {}
Correct:
class CustomException implements Exception {
final String message;
CustomException(this.message);
@override
String toString() => 'CustomException: $message';
}
Throwing Generic Exceptions
Wrong:
throw Exception('Error occurred'); // Too generic
Correct:
throw ValidationException(
field: 'email',
message: 'Invalid email format',
);
Summary
Custom exceptions provide rich error context and enable precise error handling. Use them to create domain-specific error types with meaningful messages and data.
Next Steps
Now that you understand custom exceptions, continue to:
Did You Know?
- Custom exceptions can have methods
- Exception hierarchies enable grouped handling
toString()should be overridden- Custom exceptions can carry context data
- Use
onto catch specific custom exceptions - Rich exceptions improve debugging
- Custom exceptions make code self-documenting