On this page22 sections
- Prerequisites
- Connection With Previous Lectures
- Learning Objectives
- 1. A Future Can Fail
- 2. Throwing Inside an async Function
- 3. try, on, and finally Around await
- 4. Without await, catch Catches Nothing
- 5. .catchError and onError with .then
- 6. .whenComplete — The Chain's finally
- 7. Giving Up: .timeout() and TimeoutException
- 8. Trying Again: A Retry Loop
- 9. Custom Exceptions in Async Code
- 10. A Real-World Example
- Code Examples
- Common Beginner Mistakes
- Best Practices
- Mini Challenge
- Practice Questions
- Practice Answers
- Quick Revision
- Key Takeaways
- What's Next?
Lecture 90 — Future Error Handling
| Category | 15 — Asynchronous Programming |
| Lecture | 090 of 140 |
| Estimated video length | 10 min |
| Target | Dart 3.12.2 |
| Level | Intermediate |
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 fromdart 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, andfinally(Lecture 084)throwand custom exception classes (Lecture 085)Future,Future.delayed,Future.value, and.then(Lecture 088)asyncfunctions andawait(Lecture 089)
Connection With Previous Lectures
In Lecture 089, every await got its value:
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.errorand withthrowinside anasyncfunction - Catch async errors with
try/catch/finallyaroundawait - Handle errors with
.catchError,onError, and.whenComplete - Recognize the crash caused by an unhandled async error
- Stop waiting for slow work with
.timeout()andTimeoutException - 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:
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'));
}Ali: 85
Unhandled exception:
No marks found for ZayanZayan'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:
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');
}
}Asking for the balance...
Got a Future back: no error yet
Caught: FormatException: Account number is emptyCalling 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:
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');
}
}Bad input: Account number is empty
Loading finishedfinally 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:
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');
}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:
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'),
);
}Error: Exception: No marks found for Sara
onError: Exception: No marks found for Zayan6. .whenComplete — The Chain's finally
.whenComplete runs its callback when the Future finishes, success or failure. It is the
.then version of finally:
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'));
}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:
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');
}
}Too slow: TimeoutException after 0:00:02.000000: Future not completedTo use a fallback value instead of failing, pass onTimeout. It must return the Future's
type, here a double:
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');
}Temperature: 0.08. 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:
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');
}
}
}Attempt 1 failed: Exception: Network down
Attempt 2 failed: Exception: Network down
Attempt 3: ConnectedAlways 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:
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');
}
}Declined: you need 200.0 more10. 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:
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);
}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 closedThe 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
void main() async {
try {
await Future.error('Server offline');
} catch (e) {
print('Caught: $e');
}
}Caught: Server offlineExample 2 — throw Inside async
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);
}
}Invalid argument(s): Age cannot be negativeExample 3 — on and finally
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');
}
}Not a number
DoneExample 4 — .then with .catchError
Future<String> loadName() async {
throw Exception('File missing');
}
void main() {
loadName()
.then((name) => print('Hello, $name'))
.catchError((e) => print('Failed: $e'));
}Failed: Exception: File missingExample 5 — .whenComplete on Success
void main() {
Future.delayed(Duration(milliseconds: 300), () => 'Ali')
.then((name) => print('Loaded $name'))
.whenComplete(() => print('Spinner hidden'));
}Loaded Ali
Spinner hiddenExample 6 — .timeout() with a Fallback
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);
}Hello (offline)Example 7 — Student Marks
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');
}
}
}Ali: 60
Zayan: Exception: No record for Zayan
Sara: 80Example 8 — Shopping Total
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}');
}
}Total: 900
Invalid quantity: 0Example 9 — Bank Balance
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');
}
}Rejected: Deposit must be positive
Balance: 1250.5Example 10 — Temperature Sensor with Retry
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');
}
}
}Attempt 1: Exception: Sensor busy
Temperature: 24.5Common Beginner Mistakes
1. Calling an async function inside try without await
try {
fetchBalance(''); // ✗ no await — the error escapes
} catch (e) {
print('Caught: $e');
}Unhandled exception:
FormatException: Account number is emptyawait the Future inside the try (section 4).
2. Returning nothing from .catchError on a Future<int>
Future<int> fetchMarks(String name) async {
throw Exception('No marks found for $name');
}
void main() {
// fetchMarks('Sara').catchError((e) => print('Error: $e')); // ✗
}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_errorAt 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
fetchMarks('Sara').whenComplete(() => print('Done')); // ✗ error not handledDone
Unhandled exception:
Exception: No marks found for Sara.whenComplete is cleanup only. Add .catchError or try/catch.
4. Forgetting to import dart:async
// } on TimeoutException catch (e) { // ✗ without import 'dart:async'Error: 'TimeoutException' isn't a type.5. An onTimeout that returns the wrong type
// onTimeout: () => 'unknown', // ✗ on a Future<double>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/catchwithawait, andawaitinside thetry. - Use
on SpecificExceptionfor expected errors, and a finalcatchfor the rest. - Put cleanup in
finallyor.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.
Lahore: 33.0
Finished
Exception: Unknown city: Quetta
FinishedTry it yourself before opening the solution.
Show solution
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');
}
}
}Lahore: 33.0
Finished
Exception: Unknown city: Quetta
FinishedPractice Questions
Work through these in order — they run from beginner to application to challenge.
Beginner
- Await
Future.error('Disk full')insidetry/catchand printCaught: Disk full. - Write an
asyncfunctionloadScore()that throwsException('No score'). Catch it inmainand print the error. - Use
.thenwithonErroronFuture.error('Offline')to printProblem: Offline.
Application
- Write
fetchPrice()that waits 300 ms and returns500. Use.then,.catchError, and.whenCompleteto printPrice: 500and thenDone. - Write
slowTask()that waits 3 seconds and returns'finished'. Use.timeout()of 1 second, catch theTimeoutException, and printTimed out. - Repeat question 5 with
onTimeoutso it printscancelledinstead of failing. - Write
parseMarks(String text) asyncthat returnsint.parse(text). Call it with'8o', catch onlyFormatException, and printInvalid marks. PrintCheckedinfinally.
Challenge
- Create
class EmptyCartException implements Exception {}. Writecheckout(List<int> prices) asyncthat throws it for an empty list and otherwise returns the sum. Call it with[200, 300]and[], printingTotal: 500andCart is empty. - A function
ping()fails on its first two calls and succeeds on the third with'pong'. Retry up to 3 times, printing each failure, thenResult: pong. - Retry
ping()from question 9 but allow only 2 attempts. After the loop, printGave upif no attempt succeeded.
Practice Answers
Question 1 — Solution
void main() async {
try {
await Future.error('Disk full');
} catch (e) {
print('Caught: $e');
}
}Caught: Disk fullQuestion 2 — Solution
Future<int> loadScore() async {
throw Exception('No score');
}
void main() async {
try {
print(await loadScore());
} catch (e) {
print(e);
}
}Exception: No scoreQuestion 3 — Solution
void main() {
var future = Future.error('Offline');
future.then((value) => print(value), onError: (e) => print('Problem: $e'));
}Problem: OfflineQuestion 4 — Solution
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'));
}Price: 500
DoneQuestion 5 — Solution
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');
}
}Timed outQuestion 6 — Solution
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);
}cancelledQuestion 7 — Solution
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');
}
}Invalid marks
CheckedQuestion 8 — Solution
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');
}
}
}Total: 500
Cart is emptyQuestion 9 — Solution
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');
}
}
}Attempt 1: Exception: No reply
Attempt 2: Exception: No reply
Result: pongQuestion 10 — Solution
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');
}
}Attempt 1: Exception: No reply
Attempt 2: Exception: No reply
Gave upQuick Revision
- What are the two ways a Future can complete?
- Where does an error thrown inside an
asyncfunction go? - Why does
try/catchmiss an error from a Future you didn'tawait? - What happens to the program when an async error is never handled?
- What is the difference between
.catchErrorand.whenComplete? - Which library must you import to catch
TimeoutException? - Why should a retry loop have a limit?
Key Takeaways
- A Future completes with either a value or an error.
Future.errorandthrowinside anasyncfunction both produce a failing Future.- The error appears at
await, sotry/catch/finallymust surround theawait. - An unhandled async error crashes the program, even after
mainhas finished. - With
.then, handle errors with.catchErrororonError; clean up with.whenComplete. .timeout()fails slow work with aTimeoutException(fromdart:async), or uses anonTimeoutfallback.- A capped
forloop withtry/catchretries 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.