Watch the video (10 min)
On this page22 sections
  1. Prerequisites
  2. Connection With Previous Lectures
  3. Learning Objectives
  4. 1. A Future Can Fail
  5. 2. Throwing Inside an async Function
  6. 3. try, on, and finally Around await
  7. 4. Without await, catch Catches Nothing
  8. 5. .catchError and onError with .then
  9. 6. .whenComplete — The Chain's finally
  10. 7. Giving Up: .timeout() and TimeoutException
  11. 8. Trying Again: A Retry Loop
  12. 9. Custom Exceptions in Async Code
  13. 10. A Real-World Example
  14. Code Examples
  15. Common Beginner Mistakes
  16. Best Practices
  17. Mini Challenge
  18. Practice Questions
  19. Practice Answers
  20. Quick Revision
  21. Key Takeaways
  22. What's Next?

Lecture 90 — Future Error Handling

Category15 — Asynchronous Programming
Lecture090 of 140
Estimated video length10 min
TargetDart 3.12.2
LevelIntermediate

A Future promises a value later, but the server may be down, the input may be wrong, or the answer may never come. This lecture shows how a Future fails, and how to catch that failure, clean up after it, give up on slow work, and try again.

Contents

Prerequisites · Connection · Objectives · Lesson · Code Examples · Common Mistakes · Best Practices · Mini Challenge · Practice Questions · Answers · Takeaways

Every error message in this lecture is real. Errors come from dart run, warnings from dart analyze, on Dart 3.12.2. Long stack traces are cut after their first lines, and file paths are shortened with ….


Prerequisites

Students should already understand, from Lectures 083–089:

  • try, catch, on, and finally (Lecture 084)
  • throw and custom exception classes (Lecture 085)
  • Future, Future.delayed, Future.value, and .then (Lecture 088)
  • async functions and await (Lecture 089)

Connection With Previous Lectures

In Lecture 089, every await got its value:

dart
var balance = await fetchBalance('PK-001');
print('Balance: $balance');

Real waiting goes wrong: the account might not exist, or the server might never reply. In Lecture 084, try/catch caught errors thrown on the spot, but an async error happens later, after the function has returned its Future. Where does that error go?


Learning Objectives

By the end of this lecture you will be able to:

  • Explain that a Future completes with either a value or an error
  • Create a failing Future with Future.error and with throw inside an async function
  • Catch async errors with try/catch/finally around await
  • Handle errors with .catchError, onError, and .whenComplete
  • Recognize the crash caused by an unhandled async error
  • Stop waiting for slow work with .timeout() and TimeoutException
  • Retry a failing operation with a loop
  • Throw and catch custom exceptions in async code

1. A Future Can Fail

A Future completes in one of two ways: with a value or with an error.

Analogy — ordering food for delivery. The order slip is the Future. Later, either the food arrives (a value) or the restaurant calls to say it is closed (an error). Either way, the slip gets an answer.

Future.value creates a Future that succeeds; Future.error creates one that fails:

dart
Future<int> fetchMarks(String name) {
  if (name == 'Ali') {
    return Future.value(85);
  }
  return Future.error('No marks found for $name');
}

void main() {
  fetchMarks('Ali').then((marks) => print('Ali: $marks'));
  fetchMarks('Zayan').then((marks) => print('Zayan: $marks'));
}
output
Ali: 85
Unhandled exception:
No marks found for Zayan

Zayan's Future failed, nobody handled the error, and the program crashed.


2. Throwing Inside an async Function

In an async function you don't need Future.error — just throw. Dart puts the error inside the returned Future instead of throwing it at the caller:

dart
Future<double> fetchBalance(String account) async {
  await Future.delayed(Duration(seconds: 1));
  if (account.isEmpty) {
    throw FormatException('Account number is empty');
  }
  return 1500.0;
}

void main() async {
  print('Asking for the balance...');
  var future = fetchBalance('');
  print('Got a Future back: no error yet');
  try {
    var balance = await future;
    print('Balance: $balance');
  } catch (e) {
    print('Caught: $e');
  }
}
output
Asking for the balance...
Got a Future back: no error yet
Caught: FormatException: Account number is empty

Calling fetchBalance('') did not fail. The error came out at await future, which is exactly where the try was waiting for it.


3. try, on, and finally Around await

Everything from Lecture 084 works with await: on picks the error type, and finally runs whether the Future succeeded or failed:

dart
Future<double> fetchBalance(String account) async {
  await Future.delayed(Duration(seconds: 1));
  if (account.isEmpty) {
    throw FormatException('Account number is empty');
  }
  return 1500.0;
}

void main() async {
  try {
    var balance = await fetchBalance('');
    print('Balance: $balance');
  } on FormatException catch (e) {
    print('Bad input: ${e.message}');
  } catch (e) {
    print('Something else went wrong: $e');
  } finally {
    print('Loading finished');
  }
}
output
Bad input: Account number is empty
Loading finished

finally is where you hide a loading spinner, because it runs on both paths.


4. Without await, catch Catches Nothing

try/catch only sees an async error if you await the Future inside the try. Remove the await and the error escapes:

dart
Future<double> fetchBalance(String account) async {
  await Future.delayed(Duration(seconds: 1));
  if (account.isEmpty) {
    throw FormatException('Account number is empty');
  }
  return 1500.0;
}

void main() async {
  try {
    fetchBalance('');
    print('Request sent');
  } catch (e) {
    print('Caught: $e');
  }
  print('main is done');
}
output
Request sent
main is done
Unhandled exception:
FormatException: Account number is empty
#0      fetchBalance (file:///…/bin/main.dart:4:5)
<asynchronous suspension>

The try finished instantly. One second later the Future failed with nobody listening, and the program crashed after main was done.


5. .catchError and onError with .then

When you use .then instead of await, attach the error handler to the chain. .catchError catches an error from anywhere earlier in the chain; onError is a named argument of .then itself:

dart
Future<int> fetchMarks(String name) async {
  await Future.delayed(Duration(milliseconds: 500));
  if (name != 'Ali') {
    throw Exception('No marks found for $name');
  }
  return 85;
}

void main() {
  fetchMarks('Sara')
      .then((marks) => print('Marks: $marks'))
      .catchError((e) => print('Error: $e'));

  fetchMarks('Zayan').then(
    (marks) => print('Marks: $marks'),
    onError: (e) => print('onError: $e'),
  );
}
output
Error: Exception: No marks found for Sara
onError: Exception: No marks found for Zayan

6. .whenComplete — The Chain's finally

.whenComplete runs its callback when the Future finishes, success or failure. It is the .then version of finally:

dart
Future<int> fetchMarks(String name) async {
  await Future.delayed(Duration(milliseconds: 500));
  if (name != 'Ali') {
    throw Exception('No marks found for $name');
  }
  return 85;
}

void main() {
  print('Loading...');
  fetchMarks('Sara')
      .then((marks) => print('Marks: $marks'))
      .catchError((e) => print('Error: $e'))
      .whenComplete(() => print('Hide loading spinner'));
}
output
Loading...
Error: Exception: No marks found for Sara
Hide loading spinner

.whenComplete does not handle the error. Without .catchError, the program would still crash, as Common Mistake 3 shows.


7. Giving Up: .timeout() and TimeoutException

Some Futures take too long. .timeout(duration) returns a new Future that fails with a TimeoutException if the original hasn't finished in time. TimeoutException lives in dart:async, so import it:

dart
import 'dart:async';

Future<double> fetchTemperature() async {
  await Future.delayed(Duration(seconds: 5));
  return 31.5;
}

void main() async {
  try {
    var temp = await fetchTemperature().timeout(Duration(seconds: 2));
    print('Temperature: $temp');
  } on TimeoutException catch (e) {
    print('Too slow: $e');
  }
}
output
Too slow: TimeoutException after 0:00:02.000000: Future not completed

To use a fallback value instead of failing, pass onTimeout. It must return the Future's type, here a double:

dart
Future<double> fetchTemperature() async {
  await Future.delayed(Duration(seconds: 5));
  return 31.5;
}

void main() async {
  var temp = await fetchTemperature().timeout(
    Duration(seconds: 2),
    onTimeout: () => 0.0,
  );
  print('Temperature: $temp');
}
output
Temperature: 0.0

8. Trying Again: A Retry Loop

Network errors are often temporary. A for loop with try/catch inside retries a few times, and break stops as soon as one attempt works:

dart
var calls = 0;

Future<String> connect() async {
  await Future.delayed(Duration(milliseconds: 300));
  calls++;
  if (calls < 3) {
    throw Exception('Network down');
  }
  return 'Connected';
}

void main() async {
  for (var attempt = 1; attempt <= 3; attempt++) {
    try {
      var result = await connect();
      print('Attempt $attempt: $result');
      break;
    } catch (e) {
      print('Attempt $attempt failed: $e');
    }
  }
}
output
Attempt 1 failed: Exception: Network down
Attempt 2 failed: Exception: Network down
Attempt 3: Connected

Always cap the attempts: a loop that retries forever can hang your program.


9. Custom Exceptions in Async Code

The custom exception classes from Lecture 085 work unchanged. Throw one inside an async function and catch it by type with on:

dart
class InsufficientFundsException implements Exception {
  final double balance;
  final double amount;
  InsufficientFundsException(this.balance, this.amount);
}

Future<double> withdraw(double balance, double amount) async {
  await Future.delayed(Duration(milliseconds: 500));
  if (amount > balance) {
    throw InsufficientFundsException(balance, amount);
  }
  return balance - amount;
}

void main() async {
  try {
    var left = await withdraw(1000.0, 1200.0);
    print('Left: $left');
  } on InsufficientFundsException catch (e) {
    print('Declined: you need ${e.amount - e.balance} more');
  }
}
output
Declined: you need 200.0 more

10. A Real-World Example

Sara orders from a shop whose price server is sometimes slow. The order retries on timeouts, reports an out-of-stock product, and always closes the order screen:

dart
import 'dart:async';

class OutOfStockException implements Exception {
  final String product;
  OutOfStockException(this.product);

  @override
  String toString() => '$product is out of stock';
}

var calls = 0;

Future<int> fetchPrice(String product) async {
  calls++;
  await Future.delayed(Duration(milliseconds: calls == 1 ? 3000 : 200));
  if (product == 'Bag') {
    throw OutOfStockException(product);
  }
  return 500;
}

Future<void> order(String product, int quantity) async {
  print('Ordering $quantity x $product');
  try {
    for (var attempt = 1; attempt <= 3; attempt++) {
      try {
        var price = await fetchPrice(product).timeout(Duration(seconds: 1));
        print('Total: ${price * quantity}');
        return;
      } on TimeoutException {
        print('Attempt $attempt timed out, retrying...');
      }
    }
    print('Server not responding');
  } on OutOfStockException catch (e) {
    print('Cannot order: $e');
  } finally {
    print('Order screen closed');
  }
}

void main() async {
  await order('Notebook', 3);
  await order('Bag', 1);
}
output
Ordering 3 x Notebook
Attempt 1 timed out, retrying...
Total: 1500
Order screen closed
Ordering 1 x Bag
Cannot order: Bag is out of stock
Order screen closed

The inner try catches only TimeoutException, so it retries slow calls. OutOfStockException passes through to the outer on, since retrying cannot fix it. finally runs even after return.


Code Examples

Example 1 — Awaiting Future.error

dart
void main() async {
  try {
    await Future.error('Server offline');
  } catch (e) {
    print('Caught: $e');
  }
}
output
Caught: Server offline

Example 2 — throw Inside async

dart
Future<int> checkAge(int age) async {
  if (age < 0) {
    throw ArgumentError('Age cannot be negative');
  }
  return age;
}

void main() async {
  try {
    print(await checkAge(-5));
  } catch (e) {
    print(e);
  }
}
output
Invalid argument(s): Age cannot be negative

Example 3 — on and finally

dart
Future<int> parseScore(String text) async {
  await Future.delayed(Duration(milliseconds: 200));
  return int.parse(text);
}

void main() async {
  try {
    print(await parseScore('9x'));
  } on FormatException {
    print('Not a number');
  } finally {
    print('Done');
  }
}
output
Not a number
Done

Example 4 — .then with .catchError

dart
Future<String> loadName() async {
  throw Exception('File missing');
}

void main() {
  loadName()
      .then((name) => print('Hello, $name'))
      .catchError((e) => print('Failed: $e'));
}
output
Failed: Exception: File missing

Example 5 — .whenComplete on Success

dart
void main() {
  Future.delayed(Duration(milliseconds: 300), () => 'Ali')
      .then((name) => print('Loaded $name'))
      .whenComplete(() => print('Spinner hidden'));
}
output
Loaded Ali
Spinner hidden

Example 6 — .timeout() with a Fallback

dart
Future<String> fetchGreeting() async {
  await Future.delayed(Duration(seconds: 3));
  return 'Hello from the server';
}

void main() async {
  var text = await fetchGreeting().timeout(
    Duration(seconds: 1),
    onTimeout: () => 'Hello (offline)',
  );
  print(text);
}
output
Hello (offline)

Example 7 — Student Marks

dart
Future<int> fetchMarks(String name) async {
  await Future.delayed(Duration(milliseconds: 200));
  if (name == 'Zayan') {
    throw Exception('No record for $name');
  }
  return name.length * 20;
}

void main() async {
  for (var student in ['Ali', 'Zayan', 'Sara']) {
    try {
      print('$student: ${await fetchMarks(student)}');
    } catch (e) {
      print('$student: $e');
    }
  }
}
output
Ali: 60
Zayan: Exception: No record for Zayan
Sara: 80

Example 8 — Shopping Total

dart
class InvalidQuantityException implements Exception {
  final int quantity;
  InvalidQuantityException(this.quantity);
}

Future<int> checkout(int price, int quantity) async {
  await Future.delayed(Duration(milliseconds: 300));
  if (quantity <= 0) {
    throw InvalidQuantityException(quantity);
  }
  return price * quantity;
}

void main() async {
  try {
    print('Total: ${await checkout(450, 2)}');
    print('Total: ${await checkout(450, 0)}');
  } on InvalidQuantityException catch (e) {
    print('Invalid quantity: ${e.quantity}');
  }
}
output
Total: 900
Invalid quantity: 0

Example 9 — Bank Balance

dart
Future<double> deposit(double balance, double amount) async {
  await Future.delayed(Duration(milliseconds: 300));
  if (amount <= 0) {
    throw ArgumentError('Deposit must be positive');
  }
  return balance + amount;
}

void main() async {
  var balance = 1000.0;
  try {
    balance = await deposit(balance, 250.5);
    balance = await deposit(balance, -50);
  } on ArgumentError catch (e) {
    print('Rejected: ${e.message}');
  } finally {
    print('Balance: $balance');
  }
}
output
Rejected: Deposit must be positive
Balance: 1250.5

Example 10 — Temperature Sensor with Retry

dart
var readings = 0;

Future<double> readSensor() async {
  await Future.delayed(Duration(milliseconds: 200));
  readings++;
  if (readings == 1) {
    throw Exception('Sensor busy');
  }
  return 24.5;
}

void main() async {
  for (var attempt = 1; attempt <= 3; attempt++) {
    try {
      print('Temperature: ${await readSensor()}');
      break;
    } catch (e) {
      print('Attempt $attempt: $e');
    }
  }
}
output
Attempt 1: Exception: Sensor busy
Temperature: 24.5

Common Beginner Mistakes

1. Calling an async function inside try without await

dart
try {
  fetchBalance(''); // ✗ no await — the error escapes
} catch (e) {
  print('Caught: $e');
}
output
Unhandled exception:
FormatException: Account number is empty

await the Future inside the try (section 4).

2. Returning nothing from .catchError on a Future<int>

dart
Future<int> fetchMarks(String name) async {
  throw Exception('No marks found for $name');
}

void main() {
  // fetchMarks('Sara').catchError((e) => print('Error: $e')); // ✗
}
output
warning - A value of type 'void' can't be returned by the 'onError' handler because it must
be assignable to 'FutureOr<int>'. - invalid_return_type_for_catch_error

At run time it crashes with The error handler of Future.catchError must return a value of the future's type. Put .catchError after a .then, or use try/catch with await.

3. Expecting .whenComplete to handle the error

dart
fetchMarks('Sara').whenComplete(() => print('Done')); // ✗ error not handled
output
Done
Unhandled exception:
Exception: No marks found for Sara

.whenComplete is cleanup only. Add .catchError or try/catch.

4. Forgetting to import dart:async

dart
// } on TimeoutException catch (e) { // ✗ without import 'dart:async'
output
Error: 'TimeoutException' isn't a type.

5. An onTimeout that returns the wrong type

dart
// onTimeout: () => 'unknown', // ✗ on a Future<double>
output
Error: A value of type 'String' can't be returned from a function with return type 'FutureOr<double>'.

The fallback must match the Future's type: () => 0.0.


Best Practices

  • Prefer try/catch with await, and await inside the try.
  • Use on SpecificException for expected errors, and a final catch for the rest.
  • Put cleanup in finally or .whenComplete.
  • Add .timeout() to anything that waits on a network or device.
  • Retry only temporary failures (timeouts, busy servers), and cap the attempts.

Mini Challenge

Write fetchWeather(String city) that waits 300 ms, throws Exception('Unknown city: $city') for any city except 'Lahore', and returns 33.0 for Lahore. In main, fetch Lahore and then Quetta, catching errors and printing Finished after each attempt.

output
Lahore: 33.0
Finished
Exception: Unknown city: Quetta
Finished

Try it yourself before opening the solution.

Show solution
dart
Future<double> fetchWeather(String city) async {
  await Future.delayed(Duration(milliseconds: 300));
  if (city != 'Lahore') {
    throw Exception('Unknown city: $city');
  }
  return 33.0;
}

void main() async {
  for (var city in ['Lahore', 'Quetta']) {
    try {
      print('$city: ${await fetchWeather(city)}');
    } catch (e) {
      print(e);
    } finally {
      print('Finished');
    }
  }
}
output
Lahore: 33.0
Finished
Exception: Unknown city: Quetta
Finished

Practice Questions

Work through these in order — they run from beginner to application to challenge.

Beginner

  1. Await Future.error('Disk full') inside try/catch and print Caught: Disk full.
  2. Write an async function loadScore() that throws Exception('No score'). Catch it in main and print the error.
  3. Use .then with onError on Future.error('Offline') to print Problem: Offline.

Application

  1. Write fetchPrice() that waits 300 ms and returns 500. Use .then, .catchError, and .whenComplete to print Price: 500 and then Done.
  2. Write slowTask() that waits 3 seconds and returns 'finished'. Use .timeout() of 1 second, catch the TimeoutException, and print Timed out.
  3. Repeat question 5 with onTimeout so it prints cancelled instead of failing.
  4. Write parseMarks(String text) async that returns int.parse(text). Call it with '8o', catch only FormatException, and print Invalid marks. Print Checked in finally.

Challenge

  1. Create class EmptyCartException implements Exception {}. Write checkout(List<int> prices) async that throws it for an empty list and otherwise returns the sum. Call it with [200, 300] and [], printing Total: 500 and Cart is empty.
  2. A function ping() fails on its first two calls and succeeds on the third with 'pong'. Retry up to 3 times, printing each failure, then Result: pong.
  3. Retry ping() from question 9 but allow only 2 attempts. After the loop, print Gave up if no attempt succeeded.

Practice Answers

Question 1 — Solution

dart
void main() async {
  try {
    await Future.error('Disk full');
  } catch (e) {
    print('Caught: $e');
  }
}
output
Caught: Disk full

Question 2 — Solution

dart
Future<int> loadScore() async {
  throw Exception('No score');
}

void main() async {
  try {
    print(await loadScore());
  } catch (e) {
    print(e);
  }
}
output
Exception: No score

Question 3 — Solution

dart
void main() {
  var future = Future.error('Offline');
  future.then((value) => print(value), onError: (e) => print('Problem: $e'));
}
output
Problem: Offline

Question 4 — Solution

dart
Future<int> fetchPrice() async {
  await Future.delayed(Duration(milliseconds: 300));
  return 500;
}

void main() {
  fetchPrice()
      .then((price) => print('Price: $price'))
      .catchError((e) => print('Error: $e'))
      .whenComplete(() => print('Done'));
}
output
Price: 500
Done

Question 5 — Solution

dart
import 'dart:async';

Future<String> slowTask() async {
  await Future.delayed(Duration(seconds: 3));
  return 'finished';
}

void main() async {
  try {
    print(await slowTask().timeout(Duration(seconds: 1)));
  } on TimeoutException {
    print('Timed out');
  }
}
output
Timed out

Question 6 — Solution

dart
Future<String> slowTask() async {
  await Future.delayed(Duration(seconds: 3));
  return 'finished';
}

void main() async {
  var result = await slowTask().timeout(
    Duration(seconds: 1),
    onTimeout: () => 'cancelled',
  );
  print(result);
}
output
cancelled

Question 7 — Solution

dart
Future<int> parseMarks(String text) async {
  return int.parse(text);
}

void main() async {
  try {
    print(await parseMarks('8o'));
  } on FormatException {
    print('Invalid marks');
  } finally {
    print('Checked');
  }
}
output
Invalid marks
Checked

Question 8 — Solution

dart
class EmptyCartException implements Exception {}

Future<int> checkout(List<int> prices) async {
  if (prices.isEmpty) {
    throw EmptyCartException();
  }
  var total = 0;
  for (var price in prices) {
    total += price;
  }
  return total;
}

void main() async {
  for (var cart in [
    [200, 300],
    <int>[],
  ]) {
    try {
      print('Total: ${await checkout(cart)}');
    } on EmptyCartException {
      print('Cart is empty');
    }
  }
}
output
Total: 500
Cart is empty

Question 9 — Solution

dart
var calls = 0;

Future<String> ping() async {
  calls++;
  if (calls < 3) {
    throw Exception('No reply');
  }
  return 'pong';
}

void main() async {
  for (var attempt = 1; attempt <= 3; attempt++) {
    try {
      print('Result: ${await ping()}');
      break;
    } catch (e) {
      print('Attempt $attempt: $e');
    }
  }
}
output
Attempt 1: Exception: No reply
Attempt 2: Exception: No reply
Result: pong

Question 10 — Solution

dart
var calls = 0;

Future<String> ping() async {
  calls++;
  if (calls < 3) {
    throw Exception('No reply');
  }
  return 'pong';
}

void main() async {
  var success = false;
  for (var attempt = 1; attempt <= 2; attempt++) {
    try {
      print('Result: ${await ping()}');
      success = true;
      break;
    } catch (e) {
      print('Attempt $attempt: $e');
    }
  }
  if (!success) {
    print('Gave up');
  }
}
output
Attempt 1: Exception: No reply
Attempt 2: Exception: No reply
Gave up

Quick Revision

  1. What are the two ways a Future can complete?
  2. Where does an error thrown inside an async function go?
  3. Why does try/catch miss an error from a Future you didn't await?
  4. What happens to the program when an async error is never handled?
  5. What is the difference between .catchError and .whenComplete?
  6. Which library must you import to catch TimeoutException?
  7. Why should a retry loop have a limit?

Key Takeaways

  • A Future completes with either a value or an error.
  • Future.error and throw inside an async function both produce a failing Future.
  • The error appears at await, so try/catch/finally must surround the await.
  • An unhandled async error crashes the program, even after main has finished.
  • With .then, handle errors with .catchError or onError; clean up with .whenComplete.
  • .timeout() fails slow work with a TimeoutException (from dart:async), or uses an onTimeout fallback.
  • A capped for loop with try/catch retries temporary failures.
  • Custom exceptions work in async code exactly as in synchronous code.

What's Next?

Lecture 091 — Working with Multiple Futures. So far we have waited for one Future at a time. Real apps often load several things at once: marks for every student, prices for every product. Next we will learn to start many Futures together, wait for all of them, and handle the case where one of them fails.


Video lecture

Subscribe
Placeholder video — the real recording isn't linked yet.