| |

Dart 37 ๐ŸŽฏ Handling Futures: .then(), .catchError(), and .whenComplete()

A Future on its own does nothing. It is a placeholder โ€” a value that will exist later, an error that may occur later. The future does not act. It waits for the operation it represents to complete, and when that happens, it holds the result. To use that result, you attach callbacks. The three callbacks that matter are then, catchError, and whenComplete. Together they form the complete interface for handling an asynchronous result: what to do with the value, what to do with the error, and what to do regardless of the outcome.

These three methods are not arbitrary additions to the API. They mirror the three parts of any try/catch/finally block. then is the try body: it receives the value when the operation succeeds. catchError is the catch clause: it receives the error when the operation fails. whenComplete is the finally clause: it runs whether the operation succeeded or failed, and it does not receive the value or the error. The parallel is deliberate. Future brings the structure of synchronous error handling to asynchronous code, with the same three roles .

The behavior of each method is more subtle than the analogy suggests. Each returns a new future, not the original one. The new future completes with whatever the callback returns, or with the original error if the callback throws, or with the original error if the callback is skipped. This chaining is what makes then composable: a then callback can return another future, and the chain waits for it. catchError can return a value, which makes the error “handled” and allows the chain to continue. whenComplete runs in both cases but does not change the value or error that passes through it. Understanding these rules is what separates code that handles errors correctly from code that silently swallows them .

This chapter covers three areas. First, why then, catchError, and whenComplete exist โ€” the problem of using a value that does not exist yet, and why returning new futures instead of mutating the original is the right design. Second, how each method works โ€” the signature of the then callback, the two signatures of catchError, the whenComplete cleanup semantics, and the rules for chaining and error propagation. Third, how the three compose โ€” the order in which callbacks run, how errors skip then and reach catchError, how whenComplete runs in both paths, and the difference between handling an error and letting it propagate. The chapter ends with a complete example session, a quick reference, best practices, common pitfalls, real-world examples, and diagrams showing the callback chain and error propagation.

Key point: then handles the value, catchError handles the error, and whenComplete runs in both cases for cleanup. Each returns a new future, and the chain propagates values and errors forward. An error skips every then until a catchError handles it, and whenComplete runs on every path but does not change the value or error .


Why then, catchError, and whenComplete exist

The callback attachment problem. A future completes at some point in the future, and the code that needs the result must run at that point. There is no way to “return” the result to the caller synchronously, because the result does not exist yet. The only option is to attach a callback that runs when the result arrives. then is that attachment mechanism. It says: “when this future completes with a value, run this function with that value.” Without then, a future is a value that can never be read .

The error-propagation problem. A synchronous function that throws propagates the error up the call stack until a try/catch intercepts it. An asynchronous operation cannot do this, because the call stack is gone by the time the error occurs. The error must be carried in the future itself. catchError is the interception point. When a future completes with an error, every then callback is skipped until a catchError is reached. The catchError receives the error and decides what to do: handle it and return a value, or rethrow it and let it propagate further. This is the asynchronous equivalent of the call stack unwinding .

The cleanup problem. Some code must run regardless of whether the operation succeeded or failed. A file must be closed. A loading spinner must be dismissed. A lock must be released. In synchronous code, this is the finally block. In asynchronous code, it is whenComplete. It runs after the future completes, whether with a value or an error, and it does not receive the value or the error. Its return value is ignored, and the value or error that completed the future passes through it unchanged .

The chaining problem. Asynchronous operations are often sequential. Fetch a user, then fetch the user’s posts, then fetch the posts’ comments. Each step depends on the previous step’s result. then returns a new future, which allows the steps to be chained. The callback for the first then returns a value or a future, and the next then in the chain receives that value. This is what makes asynchronous code composable without nested callbacks. Each then is a step in a pipeline, and the pipeline is a single future that completes when the last step completes .

The new-future problem. then, catchError, and whenComplete do not modify the future they are called on. They return a new future. The original future is unchanged and can have multiple callbacks attached independently. This is a deliberate design: futures are immutable values, and callbacks are attached to them, not registered on them. The new future completes with the return value of the callback. If the callback returns a future, the new future waits for that future and completes with its result. This flattening is automatic, and it is what allows chains to be built without nesting .

The trade-off. The callback API is lower-level than async/await. A chain of then calls is harder to read than a sequence of await expressions, especially when error handling is involved. catchError has two different signatures โ€” one that receives the error and one that receives the error and a stack trace โ€” and the wrong one can silently fail to type-check. whenComplete runs in both paths but cannot change the outcome, which is sometimes surprising. The trade-off is between the precision and composability of the callback API and the readability of async/await. Most Dart code should use async/await for clarity, and reach for the callback API when a future needs to be handled without making the surrounding function asynchronous.


a. then โ€” handling the value

then attaches a callback that runs when the future completes with a value. The callback receives the value and returns a new value, a future, or nothing. The future returned by then completes with whatever the callback returns .

Future<int> fetchCount() async => 42;

void main() {
  fetchCount().then((count) {
    print('count: $count');
  });
}

The callback receives 42 and prints it. The return value of the callback is void, so the future returned by then completes with null. If the callback returned a value, that value would be the completion value of the returned future:

fetchCount()
    .then((count) => count * 2)
    .then((doubled) => print('doubled: $doubled'));

The first then returns 84, so the second then receives 84. This is the chaining mechanism: the return value of one callback becomes the input of the next .

If the callback returns a future, the chain waits for that future:

fetchCount()
    .then((count) => fetchDetails(count))
    .then((details) => print(details));

The first then returns a Future<Details>. The second then does not receive the future โ€” it receives the value the future completes with. Dart flattens the nested future automatically. This is why a then callback can return either a value or a future, and the chain behaves the same way in both cases.

The then callback has two optional parameters: the value handler and the error handler. The second form is a shorthand for attaching a catchError to the returned future:

fetchCount().then(
  (count) => print('count: $count'),
  onError: (e) => print('error: $e'),
);

This is equivalent to fetchCount().then((count) => print(count)).catchError((e) => print(e)). The two-argument form is rarely used because catchError is clearer.

b. catchError โ€” handling the error

catchError attaches a callback that runs when the future completes with an error. It has two possible signatures: one that takes the error, and one that takes the error and the stack trace.

fetchCount().catchError((error) {
  print('caught: $error');
});
fetchCount().catchError((error, stackTrace) {
  print('caught: $error');
  print(stackTrace);
});

The two-argument form is required when the error handler is declared with two parameters. The onError parameter can also carry a test argument that filters which errors are handled:

fetchCount().catchError(
  (error) => print('caught: $error'),
  test: (error) => error is NetworkException,
);

The test function receives the error and returns a boolean. If it returns true, the error handler runs. If it returns false, the error is not handled by this catchError and propagates to the next handler in the chain. This allows selective error handling: catch network errors here, let other errors propagate.

The return value of the catchError callback determines what happens next. If the callback returns a value, the error is considered handled, and the future returned by catchError completes with that value. The chain continues as if the error had not occurred:

fetchCount()
    .catchError((e) => 0)
    .then((count) => print('count: $count')); // prints 0

If the callback returns nothing or rethrows, the error propagates to the next catchError in the chain. If there is no next catchError, the error becomes an unhandled asynchronous error, which is reported to the zone’s error handler โ€” in a Flutter app, this means a red error screen in debug mode and a silent failure in release mode .

An error skips every then callback until a catchError is reached. This is the asynchronous equivalent of an exception unwinding the call stack:

Future.error('boom')
    .then((v) => print('not reached'))
    .then((v) => print('not reached either'))
    .catchError((e) => print('caught: $e'));

Both then callbacks are skipped, and the catchError receives 'boom'. If the catchError returns a value, the chain continues with that value. If it rethrows, the error propagates further.

c. whenComplete โ€” running regardless of outcome

whenComplete attaches a callback that runs when the future completes, whether with a value or an error. The callback takes no arguments and its return value is ignored. The value or error that completed the future passes through unchanged .

fetchCount()
    .then((count) => print('count: $count'))
    .catchError((e) => print('error: $e'))
    .whenComplete(() => print('done'));

The whenComplete callback runs after the then or catchError callback, regardless of which one ran. It is the asynchronous equivalent of a finally block. It is used for cleanup: closing a file, hiding a spinner, releasing a lock .

whenComplete does not change the outcome. If the future completed with a value, the returned future completes with the same value. If it completed with an error, the returned future completes with the same error. The callback’s return value is discarded. If the callback throws, the error from the callback replaces the original outcome โ€” this is the one way whenComplete can affect the result.

Future.value(1)
    .whenComplete(() => print('cleanup'))
    .then((v) => print('value: $v')); // prints cleanup, then value: 1

whenComplete is often used with await in a finally block, which is the more readable equivalent:

try {
  final count = await fetchCount();
  print('count: $count');
} catch (e) {
  print('error: $e');
} finally {
  print('done');
}

The finally block runs in both paths, just like whenComplete. For most code, this form is clearer. The callback form is useful when the surrounding function cannot be async.


Complete Example Session

// ============================================
// PART 1: THEN โ€” HANDLING A VALUE
// ============================================

// then attaches a callback that runs when the
// future completes with a value.

Future<int> fetchCount() async => 42;

void main() {
  fetchCount().then((count) {
    print('count: $count'); // 42
  });
}


// ============================================
// PART 2: CHAINING THEN CALLBACKS
// ============================================

// Each then returns a new future. The return
// value of one callback becomes the input of
// the next.

fetchCount()
    .then((count) => count * 2)       // returns 84
    .then((doubled) => doubled + 1)   // returns 85
    .then((result) => print(result)); // 85


// ============================================
// PART 3: THEN RETURNING A FUTURE
// ============================================

// If the callback returns a future, the chain
// waits for it and receives its value.

Future<String> fetchName(int id) async => 'User $id';

fetchCount()
    .then((count) => fetchName(count))  // returns a Future<String>
    .then((name) => print(name));       // 'User 42'


// ============================================
// PART 4: CATCHERROR โ€” HANDLING AN ERROR
// ============================================

// catchError attaches a callback that runs when
// the future completes with an error.

Future<int> fetchFailing() async {
  throw Exception('network failed');
}

fetchFailing().catchError((error) {
  print('caught: $error'); // caught: Exception: network failed
});


// ============================================
// PART 5: THE TWO SIGNATURES OF CATCHERROR
// ============================================

// The callback can take one argument (error) or
// two (error and stack trace).

fetchFailing().catchError((error, stackTrace) {
  print('caught: $error');
  print('stack: $stackTrace');
});


// ============================================
// PART 6: ERROR SKIPS THEN
// ============================================

// An error skips every then callback until a
// catchError is reached.

Future.error('boom')
    .then((v) => print('not reached'))
    .then((v) => print('not reached either'))
    .catchError((e) => print('caught: $e')); // caught: boom


// ============================================
// PART 7: CATCHERROR RETURNING A VALUE
// ============================================

// If catchError returns a value, the error is
// handled and the chain continues with that value.

Future.error('boom')
    .catchError((e) => 0)              // handles the error
    .then((count) => print('count: $count')); // count: 0


// ============================================
// PART 8: WHENC0MPLETE โ€” RUNS IN BOTH CASES
// ============================================

// whenComplete runs whether the future completes
// with a value or an error.

Future.value(1)
    .then((v) => print('value: $v'))
    .whenComplete(() => print('cleanup'));

Future.error('boom')
    .catchError((e) => print('error: $e'))
    .whenComplete(() => print('cleanup'));

// Both print 'cleanup'.


// ============================================
// PART 9: THE FULL CHAIN
// ============================================

// The complete pattern: then, catchError, whenComplete.

fetchCount()
    .then((count) => print('count: $count'))
    .catchError((e) => print('error: $e'))
    .whenComplete(() => print('done'));

// Output:
// count: 42
// done


// ============================================
// PART 10: THE ASYNC/AWAIT EQUIVALENT
// ============================================

// The same logic with async/await and try/catch/finally.

Future<void> run() async {
  try {
    final count = await fetchCount();
    print('count: $count');
  } catch (e) {
    print('error: $e');
  } finally {
    print('done');
  }
}

// then      โ†’ try body
// catchError โ†’ catch clause
// whenComplete โ†’ finally clause

The ten parts show then for values, chaining, returning futures, catchError for errors, its two signatures, error propagation skipping then, catchError returning a value, whenComplete in both paths, the full chain, and the async/await equivalent.


Quick Reference

The Three Methods

MethodRuns whenReceivesReturns
thenFuture completes with valueThe valueNew future with callback’s return
catchErrorFuture completes with errorError (and stack trace)New future with callback’s return
whenCompleteFuture completes (either)NothingNew future with same value/error

Error Propagation

SituationBehavior
Error, no catchErrorUnhandled asynchronous error
Error, then in chainthen skipped
Error, catchError returns valueError handled, chain continues
Error, catchError rethrowsError propagates to next handler
Error, whenCompleteRuns, error passes through

catchError Signatures

SignatureUsage
(error) => ...Error only
(error, stackTrace) => ...Error and stack trace
test: (error) => boolFilter which errors are handled

then vs async/await

Callback APIasync/await
thentry body
catchErrorcatch clause
whenCompletefinally clause
Chain of thenSequence of await

Best Practices

โœ… Do This:

// Use then for values, catchError for errors
fetch().then((v) => print(v)).catchError((e) => print(e));              // โœ…
// Use whenComplete for cleanup
fetch().whenComplete(() => hideSpinner());                              // โœ…
// Return a value from catchError to handle the error
fetch().catchError((e) => 0);                                           // โœ…
// Use async/await for readability when possible
try { final v = await fetch(); } catch (e) { }                          // โœ…
// Use test to filter errors
catchError(handler, test: (e) => e is NetworkException);                // โœ…

โŒ Don’t Do This:

// Don't ignore errors
fetch().then((v) => print(v)); // no catchError                          // โŒ
// Don't use whenComplete to change the result
whenComplete(() => returnValue); // return is ignored                    // โŒ
// Don't confuse the two catchError signatures
catchError((e, st) => ...) with a one-arg call                          // โŒ
// Don't return a future without flattening
then((v) => Future.value(v)) // works, but unnecessary                  // โŒ
// Don't swallow errors silently
catchError((e) {}); // no logging                                        // โŒ

Common Pitfalls

PitfallWhy It HappensFix
Error not caughtNo catchError in chainAdd catchError or try/catch
whenComplete changes resultReturn value assumed to matterReturn value is ignored
catchError signature mismatchOne arg vs two argsMatch the callback parameters
Chain stops after errorcatchError rethrowsReturn a value to continue
Unhandled error in releaseNo error handlerAttach catchError or use runZonedGuarded
then callback not runningError skipped itError skips then until catchError
Double error handlingcatchError and try/catchUse one or the other consistently

Real-World Examples

1. then for Value

fetchUser().then((user) => print(user.name));

2. catchError for Error

fetchUser().catchError((e) => print('failed: $e'));

3. whenComplete for Cleanup

fetchUser().whenComplete(() => hideSpinner());

4. Chaining

fetchUser().then((u) => fetchPosts(u.id)).then((p) => print(p.length));

5. Return Future from then

fetchUser().then((u) => fetchPosts(u.id));

6. catchError Returns Value

fetchCount().catchError((e) => 0).then((c) => print(c));

7. catchError with test

catchError(handler, test: (e) => e is TimeoutException);

8. Error Skips then

Future.error('boom').then((v) => print(v)).catchError((e) => print(e));

9. Full Chain

fetch().then((v) => print(v)).catchError((e) => print(e)).whenComplete(() => print('done'));

10. async/await Equivalent

try { final v = await fetch(); } catch (e) { } finally { }

Visual

The Callback Chain

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  THE CALLBACK CHAIN                                          โ”‚
โ”‚                                                              โ”‚
โ”‚  fetchCount()                                                โ”‚
โ”‚    โ”‚                                                         โ”‚
โ”‚    โ–ผ                                                         โ”‚
โ”‚  .then((count) => count * 2)                                 โ”‚
โ”‚    โ”‚  receives 42, returns 84                                โ”‚
โ”‚    โ–ผ                                                         โ”‚
โ”‚  .then((doubled) => doubled + 1)                             โ”‚
โ”‚    โ”‚  receives 84, returns 85                                โ”‚
โ”‚    โ–ผ                                                         โ”‚
โ”‚  .then((result) => print(result))                            โ”‚
โ”‚    โ”‚  receives 85, prints 85                                 โ”‚
โ”‚    โ–ผ                                                         โ”‚
โ”‚  .catchError((e) => print(e))                                โ”‚
โ”‚    โ”‚  skipped (no error)                                     โ”‚
โ”‚    โ–ผ                                                         โ”‚
โ”‚  .whenComplete(() => print('done'))                          โ”‚
โ”‚       runs, prints 'done'                                    โ”‚
โ”‚                                                              โ”‚
โ”‚  Each then returns a new future. The chain is a pipeline.    โ”‚
โ”‚                                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Error Propagation

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  ERROR PROPAGATION                                           โ”‚
โ”‚                                                              โ”‚
โ”‚  Future.error('boom')                                        โ”‚
โ”‚    โ”‚                                                         โ”‚
โ”‚    โ–ผ                                                         โ”‚
โ”‚  .then((v) => print('A'))     โ† SKIPPED                      โ”‚
โ”‚    โ”‚                                                         โ”‚
โ”‚    โ–ผ                                                         โ”‚
โ”‚  .then((v) => print('B'))     โ† SKIPPED                      โ”‚
โ”‚    โ”‚                                                         โ”‚
โ”‚    โ–ผ                                                         โ”‚
โ”‚  .catchError((e) => print('caught: $e')) โ† RUNS              โ”‚
โ”‚    โ”‚  returns a value (or rethrows)                          โ”‚
โ”‚    โ–ผ                                                         โ”‚
โ”‚  .whenComplete(() => print('done')) โ† RUNS                   โ”‚
โ”‚                                                              โ”‚
โ”‚  The error skips every then until a catchError is reached.   โ”‚
โ”‚  If catchError returns a value, the chain continues with     โ”‚
โ”‚  that value. If it rethrows, the error propagates further.   โ”‚
โ”‚                                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The try/catch/finally Parallel

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  CALLBACK API              async/await                       โ”‚
โ”‚                                                              โ”‚
โ”‚  .then((v) => ...)         try {                             โ”‚
โ”‚                              final v = await fetch();        โ”‚
โ”‚                              ...                             โ”‚
โ”‚                            }                                 โ”‚
โ”‚                                                              โ”‚
โ”‚  .catchError((e) => ...)   catch (e) {                       โ”‚
โ”‚                              ...                             โ”‚
โ”‚                            }                                 โ”‚
โ”‚                                                              โ”‚
โ”‚  .whenComplete(() => ...)  finally {                         โ”‚
โ”‚                              ...                             โ”‚
โ”‚                            }                                 โ”‚
โ”‚                                                              โ”‚
โ”‚  Both forms handle the same three cases: value, error,       โ”‚
โ”‚  and cleanup. The async/await form is more readable.         โ”‚
โ”‚                                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

catchError Returns a Value

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  CATCHERROR RETURNING A VALUE                                โ”‚
โ”‚                                                              โ”‚
โ”‚  Future.error('boom')                                        โ”‚
โ”‚    โ”‚                                                         โ”‚
โ”‚    โ–ผ                                                         โ”‚
โ”‚  .catchError((e) => 0)                                       โ”‚
โ”‚    โ”‚  handles the error, returns 0                           โ”‚
โ”‚    โ–ผ                                                         โ”‚
โ”‚  .then((count) => print('count: $count'))                    โ”‚
โ”‚       runs, prints 'count: 0'                                โ”‚
โ”‚                                                              โ”‚
โ”‚  Compare with rethrowing:                                    โ”‚
โ”‚                                                              โ”‚
โ”‚  .catchError((e) => throw e)                                 โ”‚
โ”‚    โ”‚  error propagates                                       โ”‚
โ”‚    โ–ผ                                                         โ”‚
โ”‚  .then((count) => ...)  โ† SKIPPED                            โ”‚
โ”‚                                                              โ”‚
โ”‚  Returning a value handles the error. Rethrowing lets it     โ”‚
โ”‚  propagate. The choice determines whether the chain          โ”‚
โ”‚  continues or stops.                                         โ”‚
โ”‚                                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Summary

ItemValue
thenAttaches a value handler; returns a new future
catchErrorAttaches an error handler; returns a new future
whenCompleteRuns on both paths; returns a new future with same outcome
ChainingEach callback’s return becomes the next callback’s input
Returning a futureThe chain waits for it and receives its value
Error propagationErrors skip then until a catchError handles them
catchError returning a valueHandles the error; chain continues
catchError rethrowingError propagates to next handler
whenComplete returnIgnored; does not change value or error
async/await parallelthen = try, catchError = catch, whenComplete = finally

Key takeaways:

  • then handles the value, catchError handles the error, whenComplete runs in both cases. The three methods form the complete interface for handling a future’s outcome, mirroring the try/catch/finally structure of synchronous code.
  • Each method returns a new future. The original future is unchanged. The new future completes with the callback’s return value, or with the original error if the callback throws.
  • then flattens nested futures. If a then callback returns a future, the chain waits for that future and the next then receives its value, not the future itself.
  • Errors skip then until a catchError is reached. This is the asynchronous equivalent of an exception unwinding the call stack. Every then between the error and the catchError is bypassed.
  • A catchError that returns a value handles the error. The chain continues with that value as if the error had not occurred. A catchError that rethrows lets the error propagate to the next handler.
  • whenComplete does not change the outcome. It runs for cleanup, and the value or error that completed the future passes through unchanged. Its return value is ignored.
  • The two catchError signatures are not interchangeable. A callback with one parameter receives the error; a callback with two receives the error and the stack trace. The wrong signature fails at runtime.
  • async/await is usually clearer. The callback API is more precise and composable, but a try/catch/finally block with await is easier to read for most code. Use the callback API when the surrounding function cannot be async.

Remember: A future is a value that will exist later, and then, catchError, and whenComplete are the three ways to act on it. then receives the value, catchError receives the error, and whenComplete runs regardless. Each returns a new future, and the chain propagates values and errors forward. An error skips every then until a catchError handles it, and whenComplete runs on every path but does not change the outcome. The callback API and async/await express the same logic: then is the try body, catchError is the catch clause, and whenComplete is the finally block. Understanding how they map to each other makes both forms easier to read, and understanding how errors propagate through a chain makes it possible to write asynchronous code that fails loudly instead of silently.



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!