Dart 34 🎯 Exception Handling: try, catch, on, finally, and Custom Exceptions
An exception is a signal that something went wrong. It interrupts the normal flow of the program, unwinds the stack, and transfers control to the nearest handler. Dart’s exception system has four keywords: try, catch, on, and finally. The try block contains the code that might throw. The catch block handles the exception. The on clause filters by exception type. The finally block runs whether or not an exception occurred. Custom exceptions are classes that extend Exception or implement Error, and they carry the information the handler needs.
Key point: An exception is an object that is thrown with throw and caught with try/catch. The on Type clause catches only the specified type. The catch (e) clause catches everything and binds the exception to a variable. The finally block runs after the try and catch, regardless of the outcome. Custom exceptions are classes; they can extend Exception, implement Exception, or extend Error. The distinction between Exception and Error is a convention: Exception is for conditions the program should catch and handle; Error is for conditions the program should not catch, such as a null dereference or an index out of range.
Why exception handling matters
The error-propagation problem. A function that can fail has two options: return a sentinel value that indicates failure, or throw an exception. The sentinel approach requires the caller to check every return value, and forgetting the check is a silent bug. The exception approach propagates the failure automatically. The caller that can handle it catches it; the callers that cannot are skipped, and the stack unwinds to the handler.
The specificity problem. A handler that catches everything catches too much. It catches the exception it was written for and the exceptions it was not. The on clause filters by type, so the handler catches only the exception it can handle. The catch clause catches the rest.
The cleanup problem. A function that opens a file must close it. A function that acquires a lock must release it. The finally block runs whether the function succeeded, failed, or returned early. It is the place for the cleanup that must happen in every path.
The information problem. An exception carries the information about the failure: what went wrong, where, and with what data. A custom exception class carries the fields the handler needs. A generic Exception carries nothing.
The distinction problem. Dart distinguishes Exception from Error. An Exception is a condition the program should handle: a file not found, a network timeout, a validation failure. An Error is a condition the program should not handle: a null dereference, an index out of range, a type cast failure. The convention is to catch Exception and let Error propagate.
The performance problem. Exceptions are for exceptional conditions, not for control flow. A loop that throws an exception on every iteration is slower than a loop with a condition. The exception mechanism is designed for the rare path, and using it for the common path is a misuse.
a. The try, catch, and on clauses
The try block contains the code that might throw. The catch block handles the exception.
try {
var result = 10 ~/ 0;
print(result);
} catch (e) {
print('Caught: $e');
}
The 10 ~/ 0 throws an IntegerDivisionByZeroException (or an UnsupportedError in some contexts). The catch (e) catches it and binds it to e. The print runs.
The on clause filters by type.
try {
var result = 10 ~/ 0;
} on IntegerDivisionByZeroException {
print('Division by zero');
} catch (e) {
print('Other error: $e');
}
The on IntegerDivisionByZeroException catches only that type. The catch (e) catches everything else. The clauses are evaluated in order, and the first match wins.
The on clause can bind the exception to a variable.
try {
var result = 10 ~/ 0;
} on IntegerDivisionByZeroException catch (e) {
print('Caught: $e');
}
The catch (e) after the on Type binds the exception. The on Type catch (e) is the combination.
The catch clause can bind the stack trace.
try {
throw Exception('Something went wrong');
} catch (e, stackTrace) {
print('Exception: $e');
print('Stack trace: $stackTrace');
}
The second parameter of catch is the StackTrace. It is useful for logging.
The on clause can be combined with the stack trace.
try {
throw FormatException('Invalid format');
} on FormatException catch (e, stackTrace) {
print('Format error: ${e.message}');
print(stackTrace);
}
The on FormatException catch (e, stackTrace) catches the specific type and binds both the exception and the stack trace.
A try block can have multiple on clauses.
try {
riskyOperation();
} on FormatException catch (e) {
print('Format: ${e.message}');
} on IOException catch (e) {
print('IO: $e');
} catch (e) {
print('Other: $e');
}
The clauses are evaluated in order. The first matching type is used. The catch (e) at the end catches anything that is not a FormatException or an IOException.
The rethrow keyword rethrows the current exception from inside a catch block.
try {
riskyOperation();
} catch (e) {
print('Logging: $e');
rethrow; // rethrow the same exception
}
The rethrow preserves the original stack trace. The throw e would replace it with a new stack trace. The rethrow is the correct way to log and propagate.
b. The finally clause and the order of execution
The finally block runs after the try and any catch, regardless of whether an exception occurred.
try {
print('Try');
throw Exception('Error');
} catch (e) {
print('Catch: $e');
} finally {
print('Finally');
}
The output is:
Try
Catch: Exception: Error
Finally
The finally runs after the catch. It runs whether or not the exception was caught.
The finally runs even if the try block returns.
int example() {
try {
return 1;
} finally {
print('Finally');
}
}
print(example());
The output is:
Finally
1
The finally runs before the return value is delivered. The return 1 is evaluated, the finally runs, and then the value is returned.
The finally runs even if the exception is not caught.
try {
throw Exception('Error');
} finally {
print('Finally');
}
The finally runs, and then the exception propagates. The output is:
Finally
Unhandled exception: Exception: Error
The finally is the place for the cleanup that must happen in every path: closing a file, releasing a lock, stopping a timer.
var file = File('data.txt').openSync();
try {
// use the file
} finally {
file.closeSync();
}
The file is closed whether the try block succeeds, fails, or returns.
The finally can itself throw an exception. If it does, the new exception replaces the original.
try {
throw Exception('Original');
} finally {
throw Exception('Finally');
}
The output is:
Unhandled exception: Exception: Finally
The original exception is lost. The finally should not throw unless the new exception is the intended result.
The try/finally without a catch is valid. It does not catch the exception; it only ensures the cleanup.
try {
riskyOperation();
} finally {
cleanup();
}
The exception propagates after the finally runs. The pattern is used when the caller is responsible for the handling, and the current function is only responsible for the cleanup.
c. Custom exceptions
A custom exception is a class. It can extend Exception, implement Exception, or extend Error.
class ValidationException implements Exception {
final String message;
final String field;
ValidationException(this.message, this.field);
@override
String toString() => 'ValidationException: $message (field: $field)';
}
The ValidationException implements Exception and carries the message and the field. The toString is overridden for a readable representation.
The exception is thrown with throw.
void validateEmail(String email) {
if (!email.contains('@')) {
throw ValidationException('Invalid email', 'email');
}
}
The throw takes an instance of the exception. The exception is caught with on.
try {
validateEmail('invalid');
} on ValidationException catch (e) {
print('${e.field}: ${e.message}');
}
The on ValidationException catch (e) catches the custom exception and accesses its fields.
A custom exception can extend another exception.
class NetworkException implements Exception {
final String message;
final int statusCode;
NetworkException(this.message, this.statusCode);
@override
String toString() => 'NetworkException($statusCode): $message';
}
class TimeoutException extends NetworkException {
TimeoutException() : super('Request timed out', 408);
}
The TimeoutException extends NetworkException and provides a default message and status code. It is caught by on NetworkException because it is a subtype.
A custom exception can be sealed or final, which allows exhaustive switch.
sealed class AppException implements Exception {
const AppException();
}
final class NotFoundException extends AppException {
final String resource;
const NotFoundException(this.resource);
}
final class PermissionException extends AppException {
final String action;
const PermissionException(this.action);
}
The AppException is a sealed class, and the switch over it is exhaustive.
try {
riskyOperation();
} on AppException catch (e) {
switch (e) {
case NotFoundException(resource: var r):
print('Not found: $r');
case PermissionException(action: var a):
print('Permission denied: $a');
}
}
The switch handles every subtype. Adding a new subtype turns the switch into a compile error.
The distinction between Exception and Error: Exception is for conditions the program should catch and handle. Error is for conditions the program should not catch, such as a null dereference or an index out of range. The convention is to catch Exception and let Error propagate.
try {
riskyOperation();
} on Exception catch (e) {
print('Handled: $e');
}
// Error is not caught and propagates
The on Exception catches the exceptions but not the errors. The errors propagate to the top level, where the program crashes with a stack trace.
A custom error extends Error.
class ConfigurationError extends Error {
final String message;
ConfigurationError(this.message);
@override
String toString() => 'ConfigurationError: $message';
}
The ConfigurationError extends Error. It is thrown for the conditions that indicate a programming mistake, not a runtime failure the program can recover from.
Complete Example Session
// ============================================
// PART 1: BASIC TRY/CATCH
// ============================================
try {
var result = 10 ~/ 0;
} catch (e) {
print('Caught: $e');
}
// ============================================
// PART 2: ON CLAUSE
// ============================================
try {
throw FormatException('Invalid');
} on FormatException catch (e) {
print('Format: ${e.message}');
}
// ============================================
// PART 3: MULTIPLE ON CLAUSES
// ============================================
try {
riskyOperation();
} on FormatException catch (e) {
print('Format: ${e.message}');
} on IOException catch (e) {
print('IO: $e');
} catch (e) {
print('Other: $e');
}
// ============================================
// PART 4: STACK TRACE
// ============================================
try {
throw Exception('Error');
} catch (e, stackTrace) {
print('$e');
print('$stackTrace');
}
// ============================================
// PART 5: FINALLY
// ============================================
try {
print('Try');
throw Exception('Error');
} catch (e) {
print('Catch');
} finally {
print('Finally');
}
// ============================================
// PART 6: FINALLY WITH RETURN
// ============================================
int example() {
try {
return 1;
} finally {
print('Finally');
}
}
// ============================================
// PART 7: TRY/FINALLY WITHOUT CATCH
// ============================================
try {
riskyOperation();
} finally {
cleanup();
}
// ============================================
// PART 8: RETHROW
// ============================================
try {
riskyOperation();
} catch (e) {
print('Logging: $e');
rethrow;
}
// ============================================
// PART 9: CUSTOM EXCEPTION
// ============================================
class ValidationException implements Exception {
final String message;
final String field;
ValidationException(this.message, this.field);
@override
String toString() => 'ValidationException: $message (field: $field)';
}
void validateEmail(String email) {
if (!email.contains('@')) {
throw ValidationException('Invalid email', 'email');
}
}
try {
validateEmail('invalid');
} on ValidationException catch (e) {
print('${e.field}: ${e.message}');
}
// ============================================
// PART 10: SEALED EXCEPTION
// ============================================
sealed class AppException implements Exception {
const AppException();
}
final class NotFoundException extends AppException {
final String resource;
const NotFoundException(this.resource);
}
final class PermissionException extends AppException {
final String action;
const PermissionException(this.action);
}
try {
riskyOperation();
} on AppException catch (e) {
switch (e) {
case NotFoundException(resource: var r):
print('Not found: $r');
case PermissionException(action: var a):
print('Permission denied: $a');
}
}
The ten parts covered basic try/catch, the on clause, multiple on clauses, the stack trace, finally, finally with a return, try/finally without a catch, rethrow, the custom exception, and the sealed exception.
Quick Reference
Exception Keywords
| Keyword | Purpose |
|---|---|
try | Code that might throw |
catch | Handles the exception |
on | Filters by type |
finally | Runs in every path |
throw | Throws an exception |
rethrow | Rethrows the current exception |
Try Block Forms
| Form | Catches |
|---|---|
try { } catch (e) { } | Everything |
try { } on Type { } | Specific type |
try { } on Type catch (e) { } | Specific type, bound |
try { } catch (e, s) { } | Exception and stack trace |
try { } finally { } | Nothing, cleanup only |
try { } catch (e) { } finally { } | Everything, with cleanup |
Exception vs Error
| Aspect | Exception | Error |
|---|---|---|
| Purpose | Recoverable condition | Programming mistake |
| Should catch | Yes | No |
| Examples | FormatException, IOException | ArgumentError, StateError |
| Base class | Exception | Error |
Custom Exception
| Form | Example |
|---|---|
| Implement | class MyException implements Exception |
| Extend | class MyException extends SomeException |
| Fields | final String message; |
toString | @override String toString() => ... |
| Sealed | sealed class AppException implements Exception |
Best Practices
✅ Do This:
// Catch specific types
try { } on FormatException catch (e) { } // ✅
// Use finally for cleanup
try { } finally { resource.close(); } // ✅
// Use rethrow to preserve the stack trace
catch (e) { log(e); rethrow; } // ✅
// Create custom exceptions with fields
class AppException implements Exception {
final String message;
AppException(this.message);
} // ✅
// Use sealed exceptions for exhaustive handling
sealed class AppException implements Exception { } // ✅
// Bind the stack trace for logging
catch (e, stackTrace) { log(e, stackTrace); } // ✅
❌ Don’t Do This:
// Don't catch everything without a reason
try { } catch (e) { } // swallows Error too // ⚠️
// Don't use exceptions for control flow
try {
for (var i = 0; ; i++) {
if (list[i] == target) break;
}
} catch (e) { } // IndexError as control flow // ❌
// Don't throw in finally
try { } finally { throw Exception('Finally'); } // ❌
// Don't catch Error
try { } on Error catch (e) { } // ⚠️
// Don't use a generic Exception when a custom one fits
throw Exception('Invalid email'); // ⚠️
// Don't forget the stack trace when logging
catch (e) { log(e); } // no stack trace // ⚠️
// Don't ignore the caught exception silently
try { } catch (e) { } // nothing // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Catching too much | catch (e) without on | Use on Type |
| Finally throws | New exception replaces original | Don’t throw in finally |
| Stack trace lost | throw e in catch | Use rethrow |
| Custom exception not caught | Wrong type in on | Check the type |
| Exception swallowed | Empty catch | Log or handle |
| Error caught | on Error | Let Error propagate |
| Control flow via exceptions | Using exceptions for loops | Use conditions |
Real-World Examples
1. Basic Try/Catch
try {
var result = 10 ~/ 0;
} catch (e) {
print('Error: $e');
}
2. Specific Type
try {
jsonDecode('invalid');
} on FormatException catch (e) {
print('Invalid JSON: ${e.message}');
}
3. Cleanup with Finally
var file = File('data.txt').openSync();
try {
// use the file
} finally {
file.closeSync();
}
4. Rethrow
try {
riskyOperation();
} catch (e) {
logger.error(e);
rethrow;
}
5. Custom Exception
class ValidationException implements Exception {
final String message;
final String field;
ValidationException(this.message, this.field);
}
6. Sealed Exception
sealed class AppException implements Exception { }
final class NotFoundException extends AppException { }
final class PermissionException extends AppException { }
7. Exhaustive Switch
switch (e) {
case NotFoundException(resource: var r): print('Not found: $r');
case PermissionException(action: var a): print('Denied: $a');
}
8. Multiple Types
try {
riskyOperation();
} on FormatException catch (e) {
// handle format
} on IOException catch (e) {
// handle IO
}
9. Stack Trace
try {
riskyOperation();
} catch (e, stackTrace) {
print('$e\n$stackTrace');
}
10. Try/Finally Without Catch
try {
riskyOperation();
} finally {
cleanup();
}
Visual
Try/Catch/Finally Flow
┌─────────────────────────────────────────────────────────────┐
│ TRY │
│ │ │
│ ├── Success ────────────────┐ │
│ │ │ │
│ ├── Exception thrown │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ CATCH │ │
│ │ │ │ │
│ │ ├── Handle ────────────┤ │
│ │ │ │ │
│ │ └── Rethrow │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ FINALLY ◀──────────────┘ │
│ │ │ │
│ │ ▼ │
│ │ Continue or propagate │
│ │ │
│ └── FINALLY (if no exception) │
│ │ │
│ ▼ │
│ Continue │
│ │
└─────────────────────────────────────────────────────────────┘
Exception vs Error
┌─────────────────────────────────────────────────────────────┐
│ EXCEPTION │
│ │
│ Conditions the program can handle │
│ • FormatException │
│ • IOException │
│ • TimeoutException │
│ │
│ Catch these. │
│ │
├─────────────────────────────────────────────────────────────┤
│ │
│ ERROR │
│ │
│ Programming mistakes │
│ • ArgumentError │
│ • StateError │
│ • IndexError │
│ • AssertionError │
│ │
│ Let these propagate. │
│ │
└─────────────────────────────────────────────────────────────┘
Custom Exception Hierarchy
┌─────────────────────────────────────────────────────────────┐
│ sealed class AppException implements Exception { } │
│ │ │
│ ├── final class NotFoundException extends AppException│
│ │ resource: String │
│ │ │
│ ├── final class PermissionException extends AppException│
│ │ action: String │
│ │ │
│ └── final class ValidationException extends AppException│
│ field: String, message: String │
│ │
│ The switch over AppException is exhaustive. │
│ Adding a subtype turns incomplete switches into errors. │
│ │
└─────────────────────────────────────────────────────────────┘
Finally Runs in Every Path
┌─────────────────────────────────────────────────────────────┐
│ try { return 1; } finally { print('Finally'); } │
│ │
│ Output: │
│ Finally │
│ 1 │
│ │
│ try { throw Exception(); } finally { print('Finally'); } │
│ │
│ Output: │
│ Finally │
│ Unhandled exception │
│ │
│ The finally block is the cleanup that runs in every path. │
│ │
└─────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
try | Code that might throw |
catch (e) | Catch everything |
on Type | Catch specific type |
on Type catch (e) | Specific type, bound |
catch (e, s) | Exception and stack trace |
finally | Runs in every path |
throw | Throw an exception |
rethrow | Rethrow the current exception |
| Custom | Class implements Exception |
| Sealed | sealed class for exhaustive handling |
| Exception | Recoverable condition |
| Error | Programming mistake |
Key takeaways:
trycontains the code that might throw;catchhandles the exception. Theon Typeclause filters by type, and thecatch (e)clause binds the exception. The clauses are evaluated in order.finallyruns in every path. It runs after the try and catch, whether the try succeeded, failed, or returned. It is the place for cleanup: closing a file, releasing a lock, stopping a timer.rethrowpreserves the stack trace. Thethrow ein a catch block creates a new stack trace. Therethrowkeeps the original. Userethrowwhen logging and propagating.- Custom exceptions are classes. They implement
Exceptionor extend another exception. They carry the fields the handler needs. ThetoStringis overridden for a readable message. - Sealed exceptions enable exhaustive switch. A
sealed classwithfinalsubclasses lets the switch handle every case. Adding a new subclass turns the incomplete switch into a compile error. Exceptionis for handling;Erroris for propagating. The convention is to catchExceptionand letErrorpropagate. AnErrorindicates a programming mistake that should be fixed, not caught.- Exceptions are for exceptional conditions. Do not use them for control flow. A loop with a condition is faster and clearer than a loop that throws and catches on every iteration.
Remember: Exception handling is the mechanism for separating the happy path from the error path. The try block is the happy path, the catch block is the error path, and the finally block is the cleanup. The on clause is the filter, and the custom exception is the carrier of the information. Use specific catches, use finally for cleanup, use rethrow to propagate, and use sealed exceptions for the exhaustive handling. The errors that should be handled are handled; the errors that should propagate are propagated. And the cleanup happens in every path.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!