In Dart, how do `then`, `catchError` and `whenComplete` chains map onto `await` with try/catch/finally, and which should you prefer?
answer
- then ≈ code after await
- catchError ≈ catch, whenComplete ≈ finally
- then's onError misses onValue's throws
- handler must return the future's type
- Effective Dart: prefer async/await
basics
~10 sthen runs code with the value, catchError handles errors like catch, and whenComplete runs cleanup like finally. The same flow written with await inside try/catch/finally is clearer, and Effective Dart prefers it.
solid answer
~40 sEach chain method has an `await` equivalent: `f.then(onValue)` is the code after `await f`, `.catchError(handler)` is a `catch` block, and `.whenComplete(action)` is a `finally` block that runs on success or failure. Effective Dart says to **prefer async/await over raw futures**, because the chained form is harder to read and has traps. `then`'s own `onError` argument handles only errors from the original future, not exceptions thrown inside `onValue`; a `catchError` placed after the `then` catches both. `catchError` takes a bare `Function`, yet its handler's result becomes the future's value, so it must return the future's type: a `Future<int>` handler that just logs is reported by the analyzer. The typed `onError<E>` extension is the safer chained form when you do need one.
go deeper
Map then, catchError and whenComplete to the code after await, catch and finally, and know that async/await is the preferred style.
Explain why then's onError misses errors from onValue and why a catchError handler must return the future's value type.
Review chained code for these traps, move it to await with try/catch/finally, and use onError<E> where a typed chained handler is justified.
Make async/await the team default, keep chaining for thin pass-through functions, and let the analyzer's catchError diagnostics block unsafe handlers.
## The two styles side by side Dart offers two ways to consume a `Future`: **callback chaining** with methods on `Future`, and **`async`/`await`**. Each chain method has a direct counterpart: | Chain method | Called when | `await` equivalent | |---|---|---| | `then(onValue)` | the future completes with a value | the statements after `await f` | | `catchError(onError, test: ...)` | the future completes with an error | a `catch` (or `on Type catch`) block | | `whenComplete(action)` | either way | a `finally` block | ```dart // Chained Future<int> countActive(String team) => downloadTeam(team) .then((t) => t.activeCount) .catchError((Object e) => 0, test: (e) => e is DownloadException) .whenComplete(closeConnection); // With await Future<int> countActive2(String team) async { try { final t = await downloadTeam(team); return t.activeCount; } on DownloadException { return 0; } finally { closeConnection(); } } ``` Effective Dart's guideline is **PREFER async/await over using raw futures**: the `await` form reads top to bottom, uses normal `if`, loops and `try`, and makes it obvious which errors are handled where. ## Trap 1: `then`'s `onError` has a narrower scope `then` accepts an optional `onError` argument. It is called only if the **original** future fails. An exception thrown inside `onValue` goes to the future **returned by** `then`, which `onError` never sees: - `f.then(onValue, onError: handle)` misses errors thrown by `onValue`. - `f.then(onValue).catchError(handle)` catches errors from both `f` and `onValue`. In the `await` style this ambiguity disappears: everything inside the `try` block is covered. ## Trap 2: `catchError`'s handler must produce a value `catchError` is declared as `Future<T> catchError(Function onError, {bool test(Object error)?})`. The parameter is a bare `Function` because the handler may take the error alone or the error and stack trace. But the handler's **return value becomes the value of the returned future**, so for a `Future<int>` it must return an `int` (or `Future<int>`). The analyzer has dedicated diagnostics: - `invalid_return_type_for_catch_error` when the handler's return type is not assignable to the future's type; - `body_might_complete_normally_catch_error` when a block-bodied handler can end without returning a value. A handler that only logs belongs on a `Future<void>`, or should rethrow. ## The typed alternative: `onError<E>` The `FutureExtensions.onError<E extends Object>` extension method is described in the SDK as a more precisely typed `catchError`. It catches only errors of type `E` (optionally filtered by `test`), requires a handler of type `FutureOr<T> Function(E error, StackTrace stackTrace)`, and so is checked at compile time. Throwing the same error object again inside it counts as a rethrow and keeps the original stack trace. ## `whenComplete` semantics - The action runs after success **or** failure, like `finally`. - If the action returns a future, completion waits for it. - The result passes through unchanged, unless the action itself throws or its future fails, in which case that error wins. ## Rewriting a chain with `await` Converting chained code is mechanical, which is why reviewers expect it: 1. Mark the function `async` and keep its `Future<T>` return type. 2. Replace each `then((x) => ...)` with `final x = await ...;` followed by the callback's body. 3. Replace `catchError(handler, test: (e) => e is SomeException)` with `on SomeException catch (e)`; a handler that needs the stack trace becomes `catch (e, st)`. 4. Replace `whenComplete(action)` with a `finally` block that calls `action`, awaiting it if it returns a future. 5. Return the final value directly instead of returning it from the last callback. The result also gives more useful stack traces in the debugger, because each `await` point appears as a line in your own function. ## When chaining is still reasonable 1. A one-line transformation that returns a future directly, such as `return load().then((r) => r.id);`, where adding `async` would be noise; Effective Dart also says not to use `async` when it has no useful effect. 2. Code that must attach a handler immediately to a future it does not await yet. 3. Existing APIs that return futures you pass along untouched. Everywhere else, `await` with `try`/`catch`/`finally` is the house style of modern Dart and the one reviewers expect.
- Does `f.then(onValue, onError: handle)` catch an exception thrown inside `onValue`?No. `then`'s `onError` handles only an error from `f` itself. An exception thrown by `onValue` completes the future returned by `then` with that error, which `handle` never sees. Chain `.catchError(handle)` after the `then`, or rewrite with `await` inside a `try`, to cover both.
- What does the `test` parameter of `catchError` do?`test` is called with the error first; if it returns `false`, this `catchError` does not handle it and the returned future completes with the same error and stack trace. It plays the role of `on SomeType` in a `catch` clause. If omitted, it defaults to accepting every error.
saying these in an interview costs you the question
- then's onError also catches errors thrown inside onValue
- A catchError handler can simply log and return nothing on a Future<int>
- whenComplete swallows the error it runs after
- Chained futures and async/await behave differently at run time
- catchError is deprecated in Dart 3