Throwing and Catching Exceptions
Dart lets you throw any non-null object — not just subclasses of Exception or Error, though those are the conventions — and catch it with try { ... } catch (e) { ... } or, to only handle a specific type, on FormatException catch (e) { ... }. Multiple on clauses can be chained to handle different exception types differently, falling through to a general catch (e) as a last resort, and a bare throw; inside a catch block (with no expression) re-throws the currently caught exception, preserving its original stack trace for debugging.
Cricket analogy: Throwing an exception is like a batsman getting a genuine LBW dismissal — a specific, recognizable event — and Dart's on FormatException catch clauses are like an umpire's decision tree that handles LBW differently from a run-out, only falling back to a general ruling when the dismissal type doesn't match any specific rule.
class InsufficientFundsException implements Exception {
final String message;
InsufficientFundsException(this.message);
@override
String toString() => 'InsufficientFundsException: $message';
}
void withdraw(double balance, double amount) {
if (amount > balance) {
throw InsufficientFundsException(
'Cannot withdraw $amount from a balance of $balance');
}
print('Withdrew $amount successfully.');
}
void main() {
try {
withdraw(100.0, 250.0);
} on InsufficientFundsException catch (e, stackTrace) {
print('Caught: $e');
// Log stackTrace to crash reporting here.
} catch (e) {
print('Unexpected error: $e');
} finally {
print('Transaction attempt complete.');
}
}Exception vs Error: Two Different Failure Categories
Dart's convention splits failures into two categories: classes implementing the Exception interface (like FormatException or a custom InsufficientFundsException) represent expected, often recoverable failure conditions worth catching and handling, while subclasses of Error (like RangeError, ArgumentError, StateError, or TypeError) signal programming bugs — an invalid index, a violated precondition, calling a method at the wrong time — that generally shouldn't be caught and 'handled', because the right fix is correcting the code that caused them, not papering over the symptom at runtime.
Cricket analogy: An Exception is like a rain delay — an expected, recoverable disruption the umpires have a standard protocol for — while an Error is like a groundskeeper discovering the pitch was never rolled before the match, a fundamental setup failure that should be fixed before play, not worked around mid-game.
When defining a custom exception, implement the Exception interface and include a descriptive message field, plus any structured data callers might need (an error code, the invalid value) — this makes the exception far more useful to catch and act on than a generic thrown String.
finally, Stack Traces, and rethrow
A finally block runs after the try/catch regardless of whether an exception was thrown, caught, or not thrown at all, making it the right place for cleanup like closing a file handle or a database connection. The two-argument form catch (e, stackTrace) gives you the StackTrace object alongside the exception, useful for logging; if you need to log an error and then let it continue propagating, use a bare rethrow rather than throw e, because rethrow preserves the original stack trace pointing to where the error actually originated, while throw e creates a new stack trace pointing to the rethrow line, hiding the real cause.
Cricket analogy: A finally block is like ground staff covering the pitch after play regardless of the result — win, loss, or rain-abandoned. rethrow is like a referee escalating an incident to the ICC with the original umpire's report attached, not a fresh report that loses the original context.
Custom Exceptions and Async Error Handling
Custom exception classes are typically simple: class InsufficientFundsException implements Exception { final String message; InsufficientFundsException(this.message); }, giving callers a specific, catchable type instead of a generic string error. Inside async functions, errors thrown by an awaited Future surface exactly like synchronous exceptions and are caught the same way with try/catch around the await; for global, last-resort error handling of uncaught async errors (for example, in a Flutter app), you can run your app inside a custom Zone with runZonedGuarded, which intercepts errors that escape every local try/catch.
Cricket analogy: A custom InsufficientFundsException is like a specific 'timed out' dismissal rule rather than a vague 'given out' — it names exactly what happened, and runZonedGuarded is like the third umpire system catching any decision that slips past the on-field umpires, a last-resort safety net for the whole match.
Writing a bare catch (e) { } with no on clause catches everything, including Error subclasses like StateError or RangeError that usually indicate a real bug in your code. Swallowing those silently hides problems that should surface loudly during development and testing.
- throw accepts any non-null object; on SpecificType catch(e) targets a particular exception type.
- Chain multiple on clauses, falling back to a general catch(e) as a last resort.
- Exception = expected/recoverable; Error = programming bug that should be fixed, not swallowed.
- finally always executes, making it the place for cleanup like closing resources.
- Use rethrow (not throw e) to preserve the original stack trace when re-raising a caught exception.
- Custom exception classes implementing Exception give callers a specific, catchable type.
- Avoid bare catch(e) without an on clause — it can silently swallow real bugs signaled by Error subclasses.
Practice what you learned
1. In `on FormatException catch (e) { ... }`, what does the `on` clause do?
2. What's the key difference Dart's conventions draw between Exception and Error?
3. What does a `finally` block guarantee?
4. Why does `rethrow` preserve more debugging information than `throw e`?
5. What's a common pitfall of writing `catch (e) { ... }` without an `on` clause?
Was this page helpful?
You May Also Like
Futures and async/await
Learn how Dart represents asynchronous computations with Future objects and how async/await syntax lets you write non-blocking code that reads like synchronous code.
Generics in Dart
Learn how Dart's generic types let you write reusable, type-safe classes and functions that work across many types without sacrificing compile-time checking.
Streams in Dart
Understand how Dart Streams model sequences of asynchronous events, the difference between single-subscription and broadcast streams, and how to consume them with listen() and await-for.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics