Dart Asynchronous Programming
Asynchronous programming is one of the most core abilities in modern application development.
Whether it's network requests, file reading and writing, or database operations, asynchronous processing is needed to avoid blocking the main thread.
This chapter introduces the concept of Future, the async/await syntax, and how to handle asynchronous errors.
Why asynchronous programming is needed
First, understand the difference between synchronous and asynchronous.
Synchronous code executes line by line in order; the next line does not start until the previous line finishes.
If an operation needs to wait (such as a network request), the entire program will get stuck.
Example
Compare the execution effects of synchronous and asynchronous code:
String fetchDataSync() {
// Synchronous code: suppose we need to wait 2 seconds here
// During these 2 seconds, the whole program cannot do anything
sleep(Duration(seconds: 2)); // Blocking wait
return 'Data fetching complete';
}
// Asynchronous version: does not block the main thread
Future<String> fetchDataAsync() async {
// Asynchronous wait: during these 2 seconds, the program can continue executing other tasks
await Future.delayed(Duration(seconds: 2));
return 'Data fetching complete';
}
void main() async {
print('Start fetching data synchronously...');
var syncResult = fetchDataSync();
print('Synchronous result: $syncResult');
print('(Note: the program is stuck during synchronous execution)');
print('\n'Start fetching data asynchronously...');
print('After initiating the request, the program continues to execute other tasks...');
var asyncResult = await fetchDataAsync();
print('Asynchronous result: $asyncResult');
print('(Note: the program is not stuck during asynchronous execution)');
}
Starting to fetch data synchronously... Synchronous result: Data fetch complete (Note: The program is stuck during synchronous execution) Starting to fetch data asynchronously... After sending the request, the program continues executing other tasks... Asynchronous result: Data fetch complete (The program is not stuck during asynchronous execution)
In Dart, all I/O operations (files, network, database) should use asynchronous methods. Synchronous I/O blocks the entire Isolate, which in Flutter applications causes UI jank.
The Concept of Future
Future
It's like a promise: I don't have a result now, but I will definitely give you a value of type T (or an error) in the future.
Future has three states:
| State | Description |
|---|---|
| Uncompleted | The asynchronous operation is still in progress |
| Completed with data | The operation succeeded, and the Future carries the result value |
| Completed but with errors (Completed with error) | The operation fails, and the Future carries the error information. |
Example
Creating and using a Future:
print('Program start');
// Create a Future
Future<String> future = Future(() {
// This function will execute at some point in the future
return 'EXAMPLE async result';
});
print('Future created, but not yet completed');
// Register callback: execute when the Future completes
future.then((result) {
print('Received result: $result');
});
print('Program continues...');
}
程序开始 Future 已创建,但还没完成 程序继续执行... 收到结果: EXAMPLE 异步结果
Note the order of the output: "program continues executing" is printed first, and "result received" is printed afterward.
This shows that Future callbacks are executed asynchronously and do not block subsequent code.
async / await syntax
async and await are the core syntax for handling asynchronous operations in Dart.
async marks a function as asynchronous, and await waits for a Future to complete and retrieves its result.
Example
// The return type of an async function must be Future<T>
Future<String> fetchUserName(int id) async {
// await waits for the Future to complete and gets its value
// While waiting, the current function pauses execution, but does not block other code
await Future.delayed(Duration(seconds: 1));
return 'User$id';
}
Future<int> fetchUserAge(int id) async {
await Future.delayed(Duration(milliseconds: 500));
return 20 + id;
}
// Execute multiple asynchronous operations sequentially
Future<void> printUserInfo(int id) async {
print('Start fetching user information...');
// Wait one by one: first fetch the name, then fetch the age
var name = await fetchUserName(id);
print('Fetched name: $name');
var age = await fetchUserAge(id);
print('Fetched age: $age');
print('EXAMPLE user: $name, age: $age');
}
// Execute multiple asynchronous operations concurrently
Future<void> printUserInfoParallel(int id) async {
print(Start fetching user info concurrently...);
// Initiate two requests simultaneously without waiting for each other
var nameFuture = fetchUserName(id);
var ageFuture = fetchUserAge(id);
// Wait for both requests to complete
var results = await Future.wait([nameFuture, ageFuture]);
var name = results[0] as String;
var age = results[1] as int;
print(EXAMPLE user (concurrent): $name, age: $age);
}
void main() async {
// Sequential execution (total time ≈ 1s + 0.5s = 1.5s)
await printUserInfo(1);
print('---');
// Concurrent execution (total time ≈ max(1s, 0.5s) = 1s)
await printUserInfoParallel(2);
}
开始获取用户信息... 获取到名称: 用户1 获取到年龄: 21 EXAMPLE 用户: 用户1, 年龄: 21 --- 开始并发获取用户信息... EXAMPLE 用户(并发): 用户2, 年龄: 22
If multiple asynchronous operations have no dependencies between them, you should use Future.wait to run them concurrently instead of awaiting them one by one. This can significantly improve performance—the total time equals the slowest operation, not the sum of all operations.
Core rules of async/await
| Rules | Description |
|---|---|
| async functions return a Future | Even if the function returns T, after using async it is automatically wrapped into Future<T>. |
| await can only be used inside async functions. | Regular functions cannot use await |
| await pauses the current function | But it does not block the execution of other code |
| await gets the result of the Future | If the Future has an error, await will throw an exception. |
then / catchError chained call
In addition to async/await, Dart also supports chaining calls using then() and catchError().
This is the traditional Promise-style writing.
Example
Future<String> fetchData(String url) {
return Future.delayed(Duration(seconds: 1), () {
if (url.isEmpty) {
throw Exception(URL cannot be empty);
}
return Data from $url;
});
}
void main() {
print(Starting request...);
// then / catchError chained call
fetchData('https://example.com/api')
.then((data) {
// Request succeeded
print(Got data: $data);
return Processed: $data; // The return value will be passed to the next then
})
.then((processed) {
// Handle the return value of the previous then
print(processed);
})
.catchError((error) {
// Catch any error in the chain uniformly
print(Request error: $error);
})
.whenComplete(() {
// Executes regardless of success or failure (similar to finally)
print(Request ended (regardless of success or failure));
});
print(Request has been sent, program continues...);
}
开始请求... 请求已发出,程序继续... 获取到数据: 来自 https://example.com/api 的数据 处理后的: 来自 https://example.com/api 的数据 请求结束(无论成功或失败)
How to choose between async/await and then/catchError?
| Scenarios | Recommended approach | Reason |
|---|---|---|
| Linear Asynchronous Flow | async/await | The code is as clear as synchronous code |
| Concurrent Multiple Requests | async/await + Future.wait | Clear semantics |
| Chained Data Transformation | then() | Each then performs one step of transformation, and the chain is clear. |
| Single callback | then() | More concise than async/await |
In most cases, async/await is recommended because it has better readability. But then() is still valuable in simple chained processing. The two can be mixed, but it's not recommended—a function should either use async/await or then/catchError; mixing them makes the code hard to understand.
Handling Asynchronous Errors
Errors may occur in asynchronous operations, so try-catch needs to be used to handle them.
async/await makes error handling as simple as synchronous code.
Example
Future<String> fetchUserProfile(int userId) async {
await Future.delayed(Duration(seconds: 1));
if (userId <= 0) {
throw Exception('Invalid user ID: $userId');
}
if (userId == 404) {
throw HttpException('User does not exist');
}
return 'Profile of user $userId';
}
// Custom exception
class HttpException implements Exception {
final String message;
HttpException(this.message);
@override
String toString() => 'HttpException: $message';
}
Future<void> loadUserProfile(int userId) async {
print('Loading profile of user $userId...');
try {
var profile = await fetchUserProfile(userId);
print('Loaded successfully: $profile');
} on HttpException catch (e) {
// Catch specific async errors by type
print('HTTP error: $e');
print('Prompt the user: please check whether the user exists');
} on Exception catch (e) {
// Catch other exceptions
print('General error: $e');
} finally {
print('Loading operation ended');
}
}
void main() async {
await loadUserProfile(1); // Success
print('---');
await loadUserProfile(404); // HttpException
print('---');
await loadUserProfile(-1); // General Exception
}
正在加载用户 1 的资料... 加载成功: 用户 1 的个人资料 加载操作结束 --- 正在加载用户 404 的资料... HTTP 错误: HttpException: 用户不存在 提示用户:请检查用户是否存在 加载操作结束 --- 正在加载用户 -1 的资料... 一般错误: Exception: 无效的用户 ID: -1 加载操作结束
Error handling rules in async functions:
- If an exception is thrown in an async function, it will be automatically wrapped into the Future.
- The caller can use try-catch to catch the exception from await.
- If then() is used, exceptions are passed to catchError().
- Uncaught async exceptions do not cause the program to crash, but it will be treated as an unhandled Future error.
other extensionsNever ignore asynchronous errors. Every async function call should have corresponding error handling. Unhandled asynchronous errors print warnings in development mode, and may be silently ignored in production environments, leading to bugs that are hard to track down.