Why Custom Exceptions?
Dart's built-in exceptions (FormatException, RangeError, etc.) cover common cases, but domain-specific errors need custom exception classes to communicate meaningful context. A PaymentDeclinedException is far more informative than a generic Exception.
Creating Custom Exceptions
// Simplest form — implement Exception interface
class ValidationException implements Exception {
final String field;
final String message;
ValidationException(this.field, this.message);
@override
String toString() => 'ValidationException[$field]: $message';
}
// With a factory constructor
class ApiException implements Exception {
final int statusCode;
final String message;
final Map<String, dynamic>? details;
const ApiException(this.statusCode, this.message, {this.details});
factory ApiException.notFound(String resource) =>
ApiException(404, '$resource not found');
factory ApiException.unauthorized() =>
ApiException(401, 'Authentication required');
factory ApiException.serverError([String? detail]) =>
ApiException(500, 'Internal server error', details: {'detail': detail});
bool get isClientError => statusCode >= 400 && statusCode < 500;
bool get isServerError => statusCode >= 500;
@override
String toString() => 'ApiException($statusCode): $message';
}
Exception Hierarchy
class AppException implements Exception {
final String message;
final String? code;
AppException(this.message, {this.code});
@override String toString() => code != null
? '[$code] $message' : message;
}
class AuthException extends AppException {
AuthException(String msg) : super(msg, code: 'AUTH_ERROR');
}
class NetworkException extends AppException {
final int statusCode;
NetworkException(String msg, this.statusCode)
: super(msg, code: 'NET_$statusCode');
}
class DatabaseException extends AppException {
DatabaseException(String msg) : super(msg, code: 'DB_ERROR');
}
Full Example
class UserService {
final Map<String, String> _users = {'alice': 'pass123'};
String login(String username, String password) {
if (username.isEmpty) throw ValidationException('username', 'Cannot be empty');
if (password.length < 6) throw ValidationException('password', 'Min 6 chars');
if (!_users.containsKey(username)) throw ApiException.notFound('User');
if (_users[username] != password) throw AuthException('Invalid password');
return 'token_$${username}_$${DateTime.now().millisecondsSinceEpoch}';
}
}
void main() {
var service = UserService();
final tests = [
('', 'pass123'),
('alice', 'abc'),
('bob', 'pass123'),
('alice', 'wrong'),
('alice', 'pass123'),
];
for (var (user, pass) in tests) {
try {
var token = service.login(user, pass);
print('Login success: $token');
} on ValidationException catch (e) {
print('Validation: $e');
} on ApiException catch (e) {
print('API: $e');
} on AuthException catch (e) {
print('Auth: $e');
}
}
}
Output:Validation: ValidationException[username]: Cannot be empty
Validation: ValidationException[password]: Min 6 chars
API: ApiException(404): User not found
Auth: [AUTH_ERROR] Invalid password
Login success: token_alice_...
🧠 Quiz
1. How do you create a custom exception in Dart?
- A) extends Exception
- B) implements Exception ✅
- C) mixin Exception
- D) abstract class Exception
2. What is the benefit of an exception hierarchy?
- A) Makes code run faster
- B) Allows catching at different granularity levels ✅
- C) Removes the need for try-catch
- D) Enforces compile-time error checking
Summary
Create custom exceptions by implementing the Exception interface. Override toString() for meaningful messages. Use factory constructors for common error scenarios. Build exception hierarchies with extends so callers can catch at the right level of specificity.