In Dart, how do try, on, catch (e, st) and finally fit together, and when do you use on versus catch?
answer
- clauses tried top to bottom
- on filters, catch binds
- first matching type wins
- second catch parameter: StackTrace
- finally runs before propagation continues
basics
~20 sIn Dart, on Type picks which thrown objects a clause handles, catch (e, st) binds the object and its StackTrace, the first matching clause in source order wins, and finally runs whether or not anything was thrown.
solid answer
~40 sA `try` block is followed by any number of catch clauses and an optional `finally`. A clause can filter with `on SomeType`, bind with `catch (e)` or `catch (e, st)`, or do both: `on FormatException catch (e, st)`. Use `on` when the type decides whether you can handle it, and `catch` when the handler needs the object or its `StackTrace`. Dart tests the clauses in source order and runs only the first whose type matches, so specific types go above `on Exception` and a bare `catch`. If nothing matches, the object keeps propagating. `finally` runs after the try block and any matching clause, and before an unmatched object leaves, so it is the place for cleanup. All Dart exceptions are unchecked: no signature declares them and the compiler never forces a catch.
code
dart · 16 linesint? parseQuantity(String cell) {
try {
return int.parse(cell);
} on FormatException catch (e, st) {
print('bad quantity "$cell": ${e.message}');
print(st);
return null;
} finally {
print('checked "$cell"');
}
}
void main() {
print(parseQuantity('3')); // checked "3", then 3
print(parseQuantity('x')); // bad quantity..., trace, checked "x", then null
}go deeper
Recall the four shapes: on Type, catch (e), catch (e, st), and on Type catch (e, st), plus finally for cleanup. Know that the first matching clause wins.
Explain that clauses are matched in source order by runtime type, what static type e gets, and exactly when finally runs relative to propagation.
Show you order clauses from specific to general, keep stack traces when logging, and never let control flow in finally swallow a failure.
Be ready to set a team rule: which lints enforce catch discipline, and how documented throws contracts replace the checked exceptions Dart does not have.
## The pieces of a try statement Dart's **try statement** has one `try` block and at least one `on`, `catch` or `finally` clause after it; a bare `try { }` is a compile-time error (`missing_catch_or_finally`). The clause forms are: | Form | Filters by type? | Binds the object? | Binds the stack trace? | |---|---|---|---| | `on FormatException { … }` | yes | no | no | | `catch (e) { … }` | no — matches anything | yes, as `Object` | no | | `catch (e, st) { … }` | no — matches anything | yes, as `Object` | yes, as `StackTrace` | | `on FormatException catch (e, st) { … }` | yes | yes, as `FormatException` | yes | | `finally { … }` | — | — | — | So **`on` answers "which failures can I handle?"** and **`catch` answers "what do I need to read from the failure?"**. When the handler only reacts to the type — skip the row, show a fixed message — `on` alone is enough. When it needs the message, a field or the trace, add `catch`. ## How Dart picks a clause When something is thrown inside the `try` block: 1. Dart looks at the **runtime type** of the thrown object. 2. It walks the clauses **in source order** and runs the **first** one whose type the object is a subtype of. A bare `catch` matches anything. 3. Only that one clause runs. Dart does not rank the clauses by specificity. 4. If no clause matches, the `finally` block (if any) runs and the object continues to propagate to the caller. If nothing up the stack catches it, the isolate that raised it is typically terminated. That is why ordering matters. If `on Exception catch (e)` sits above `on FormatException catch (e)`, the second clause can never run, because every `FormatException` is an `Exception`. The code still compiles; the analyzer reports the later clause as dead code (`dead_code_on_catch_subtype`, and `dead_code_catch_following_catch` for clauses after a bare `catch`). Put the most specific types first. ## The two catch parameters - **`e`** is the thrown object. With `on T catch (e)` its static type is `T`; with a bare `catch (e)` it is `Object`, because under sound null safety nothing thrown can be `null`. - **`st`** is a **`StackTrace`** — the call stack from the throw point. Its `toString()` gives the printable trace; the exact format is not fixed, so log it rather than parse it. - To get a trace without throwing, `StackTrace.current` returns the current one. - The analyzer flags parameters you never read (`unused_catch_clause`, `unused_catch_stack`), so declare `st` only when you log or forward it. ## What finally guarantees A **`finally`** block runs when the try statement is left in any of these ways: - the `try` block completes normally; - the `try` block executes `return`, `break` or `continue`; - something is thrown and a clause catches it (`finally` runs after that clause); - something is thrown and no clause matches (`finally` runs, then propagation continues). Use it to release resources — close a file, stop a stopwatch, clear a busy flag. Do not put `return`, `break` or `throw` inside `finally`: an abrupt exit from `finally` replaces whatever was in flight, so a pending exception silently disappears. The `control_flow_in_finally` lint, in the recommended and flutter lint sets, catches this. ```dart int? parseQuantity(String cell) { try { return int.parse(cell); } on FormatException catch (e) { print('bad quantity "$cell": ${e.message}'); return null; } finally { print('checked "$cell"'); // runs on both paths } } ``` ## Unchecked exceptions Dart has **no checked exceptions**. A function's signature never lists what it throws, and callers are never forced to catch. The contract lives in documentation — the SDK writes "Throws a [FormatException] if…" in doc comments — so reading those comments is how you learn which `on` clauses a call needs. ## Mistakes interviewers listen for - Expecting the most specific clause to win regardless of order. - Using a bare `catch (e)` everywhere, which also swallows bugs such as `ArgumentError` and failed asserts. - Writing `catch (e, st)` and never using `st`, or dropping the trace when logging. - Returning from `finally` and wondering where the exception went.
- In Dart, what is the static type of e in a bare catch (e), and why is it not nullable?It is `Object`. Under sound null safety a throw expression must be assignable to `Object`, so `throw null` or throwing a `String?` is a compile-time error, and an `on` clause cannot name a nullable type either. Nothing that reaches a catch can therefore be `null`.
- In Dart, what happens to an in-flight exception if the finally block executes return?It is discarded. An abrupt exit from `finally` — `return`, `break`, `continue` or a new `throw` — replaces the pending completion, so the original exception never reaches the caller. The `control_flow_in_finally` lint in the recommended set warns about exactly this.
saying these in an interview costs you the question
- Dart picks the most specific matching on clause, whatever the order.
- Every Dart method must declare or catch the exceptions it throws.
- finally runs only when an exception was thrown.
- A bare catch (e) catches only Exception subtypes, never Errors.
- The stack trace is available as a field on every thrown object.