Dart Futures: Chaining Async Work with .then()
A Dart Future is a promise for a value that isn't ready yet. Chain actions onto it with .then() for success and .catchError() for failure. This is key for network requests or file I/O. The footgun is forgetting .catchError(), causing silent failures.
WHY IT EXISTS In a user-facing app, long-running tasks like fetching data from the internet cannot block the main thread; otherwise, the UI would freeze. Futures provide a way to initiate a task, let the app continue running, and handle the result or error later when it's ready.
THE MENTAL MODEL Think of a Future as a receipt for a task you've started but hasn't finished. This receipt (the Future object) promises to eventually contain either the task's successful result or an error. You don't wait around for the result; you give the receipt instructions on what to do once the result is available.
HOW IT WORKS When you call a function that returns a Future, you get the Future object immediately. You then chain callback functions to it. The .then() method registers a callback that runs if the Future completes successfully, receiving the result as an argument. The .catchError() method registers a callback for when the Future completes with an error. You can chain multiple .then() calls, passing the result of one to the next. Finally, .whenComplete() registers a callback that runs regardless of success or failure, similar to a finally block.
WHEN TO USE IT Use Future chaining for any asynchronous operation where you need to perform a sequence of steps. This is common for API calls where you fetch data, then parse it, then save it to a database. It's the primary alternative to async/await syntax and is useful when you need to manage a complex flow of callbacks explicitly.
WHEN NOT TO USE IT For simple, linear asynchronous code, async/await syntax is often more readable and less prone to nesting errors than long .then() chains. Avoid chaining if you find yourself in "callback hell" with deeply nested .then() calls; refactor to async/await instead. Also, don't use it for continuous streams of data; use the Stream API for that.
ONE CANONICAL EXAMPLE A common pattern is fetching and decoding JSON. A function fetchUserData() returns a Future<Response>. You would chain it like this: fetchUserData().then((response) => jsonDecode(response.body)).then((userJson) => print('User: userJson')).catchError((error) => print('Failed to fetch user: error'));. This chain attempts the fetch, then decodes the body, then prints the user, with a single error handler for any failure along the way.
Read the original → dart.dev
Get five bites like this every day.
Tezvyn delivers a daily feed of 60-second tech bites with quizzes to lock in what you learn.