Flutter 基础体系 · 第 28/80 篇。示例基于当前稳定 Flutter 与 Dart 3 语言能力;Android、iOS、桌面和 Web 差异会明确说明。

Dart Future:Event Loop、async/await、错误、超时和并发组合

Future 是 Dart 表示“一次异步结果”的核心抽象。它可能在稍后产生一个值,也可能以错误结束;async/await 只是建立在 Future 之上的语法,而不是另一套并发模型。要正确使用它,必须同时理解四件事:

  1. 一个 isolate 如何调度同步代码、微任务和事件;
  2. Future 如何从未完成变为成功或失败;
  3. async/await 如何传播值和错误;
  4. 多个异步操作如何组合、超时以及停止或继续执行。

Dart 的异步代码通常不会自动创建线程。单个 isolate 内的 Dart 代码仍然一次只执行一段同步代码;异步的“并发”主要表现为多个 I/O 操作交错等待。需要真正利用多个 CPU 核心执行 Dart 代码时,应使用 isolate,而不是简单地把代码包装成 Future

Future 是一个一次性结果容器

可以把 Future<T> 抽象成如下状态机:

              ┌──────────────┐
              │   未完成      │
              │ pending      │
              └──────┬───────┘
                     │
          ┌──────────┴──────────┐
          │                     │
          ▼                     ▼
┌──────────────────┐  ┌──────────────────┐
│ 完成并产生 T      │  │ 完成并产生错误    │
│ value             │  │ error            │
└──────────────────┘  └──────────────────┘

一个 Future<T>

  • 最多完成一次;
  • 完成后不会再次变成未完成;
  • 可能成功产生一个 T
  • 也可能失败产生一个错误和堆栈;
  • 可以有多个监听者,但这些监听者不会改变它的完成结果。

例如:

Future<int> loadCount() {
  return Future<int>.delayed(
    const Duration(milliseconds: 100),
    () => 42,
  );
}

调用 loadCount() 时,函数立即返回一个尚未完成的 Future<int>。约 100 毫秒后,该 Future 成功完成,结果为 42

这里的 100 是调度器至少等待的延迟,不是严格的实时保证。设备负载、浏览器调度、应用是否进入后台以及线程是否被阻塞,都可能使实际回调更晚执行。

Future.valueFuture.errorFuture.delayed

这三个构造器经常用于区分“结果已知”和“稍后完成”:

final a = Future<int>.value(1);
final b = Future<int>.error(StateError('加载失败'));
final c = Future<int>.delayed(
  const Duration(seconds: 1),
  () => 3,
);

Future.value(1) 表示结果已经确定,但后续的 then 回调仍然按照 Future 的异步语义调度,不会把回调直接插入当前同步调用栈。Future.error 同样表示一个已经确定的失败结果;如果没有监听它,最终可能成为未处理异步错误。

Future.delayed 使用定时器把计算放到将来的事件中。它不是睡眠当前线程,也不会阻塞当前 isolate。

Event Loop:同步代码、微任务和事件队列

一个 Dart isolate 可以近似理解为拥有以下执行过程:

执行当前同步代码
        │
        ▼
清空 microtask queue
        │
        ▼
取出一个 event queue 事件并执行
        │
        ▼
再次清空 microtask queue
        │
        └───────────────循环

这里有两个重要队列:

  • microtask queue(微任务队列):优先级更高,通常用于继续执行 Future 链和 await 之后的代码;
  • event queue(事件队列):用于定时器、I/O、用户输入、系统消息等事件。

每次取出一个事件执行后,事件循环会先处理微任务,直到微任务队列为空,才继续处理下一个事件。因此,微任务可以延迟事件队列中的其他工作。

一个可运行的调度示例

import 'dart:async';

void main() {
  print('A');

  scheduleMicrotask(() {
    print('B: scheduleMicrotask');
  });

  Future.microtask(() {
    print('C: Future.microtask');
  });

  Future(() {
    print('D: Future');
  });

  Future.delayed(Duration.zero, () {
    print('E: Future.delayed');
  });

  print('F');
}

在常见 Dart VM 实现中,输出顺序是:

A
F
B: scheduleMicrotask
C: Future.microtask
D: Future
E: Future.delayed

逐步分析如下:

  1. print('A') 同步执行;
  2. 两个微任务被加入微任务队列;
  3. FutureFuture.delayed(Duration.zero) 的回调被安排到事件队列;
  4. print('F') 仍然属于当前同步代码,所以先输出;
  5. 当前同步代码结束后,事件循环清空微任务队列,输出 BC
  6. 最后依次处理事件队列中的 DE

这个顺序体现的是调度类别,而不是“零延时等同于立即执行”。Duration.zero 仍然需要等当前同步代码和当前批次微任务结束后才可能运行。

微任务饥饿

如果微任务不断创建新的微任务,事件队列可能长时间得不到处理:

import 'dart:async';

void loop() {
  scheduleMicrotask(loop);
}

void main() {
  loop();

  Timer.run(() {
    print('这个事件可能长期无法执行');
  });
}

loop 每次执行时都会再次加入微任务。由于事件循环必须先清空微任务队列,Timer.run 对应的事件可能一直没有机会执行。这称为微任务饥饿。

因此,微任务适合安排短小的后续工作,不适合承载持续循环、大量计算或无限递归。长计算会阻塞整个 isolate;即使把它放入微任务,也不会变成并行计算。

Future 事件与 Flutter 帧调度

在 Flutter 中,UI isolate 除了处理 Dart 的异步任务,还要参与构建 widget、布局、绘制和平台消息处理。Future 回调可以在这些工作之间运行,但不能把耗时计算变成后台线程。

例如:

Future<void> badWork() async {
  // 这段同步计算仍然阻塞当前 isolate
  var total = 0;
  for (var i = 0; i < 1000000000; i++) {
    total += i;
  }
  print(total);
}

即使调用方式是:

await badWork();

循环也仍然在当前 isolate 执行。await 只能等待 Future,不会自动把同步计算迁移到后台。

在移动端和桌面端,Flutter 的 UI isolate 被阻塞时,表现通常是掉帧、触摸无响应或动画停顿。对于 CPU 密集型任务,需要考虑 isolate;对于网络、文件或数据库等异步 I/O,应使用相应的异步 API。

Flutter Web 的定时器和事件调度还受浏览器主线程、页面后台限速和浏览器事件循环影响;不能把移动端或 Dart VM 上观察到的精确时间顺序当作 Web 上的实时保证。

async 函数:返回 Future 的函数

async 修饰的函数会返回一个 Future:

Future<String> fetchName() async {
  return 'Ada';
}

它等价于“异步地完成一个 Future<String>”,而不是返回普通的 String

final name = fetchName(); // 类型是 Future<String>

正确获取结果的方式是:

final name = await fetchName();
print(name);

或使用 Future 链:

fetchName().then((name) {
  print(name);
});

async 函数的调用会开始执行函数体中的同步部分,但它的最终结果通过 Future 暴露。遇到 await 后,函数暂停,把控制权交还给事件循环;等待的 Future 完成后,函数从暂停点继续。

Future<int> calculate() async {
  print('1');

  final value = await Future<int>.value(10);

  print('2');
  return value * 2;
}

Future<void> main() async {
  print('A');

  final future = calculate();

  print('B');

  final result = await future;
  print(result);
}

典型输出为:

A
1
B
2
20

原因是:

  1. 调用 calculate() 后,函数先同步执行到 await
  2. Future.value(10) 已经有结果,但 await 后的继续部分仍以异步方式恢复;
  3. calculate() 返回 Future,调用方继续输出 B
  4. 当前同步代码完成后,calculate() 继续执行,输出 2
  5. 最后外层 await 得到 20

“已完成的 Future”不等于“后续代码同步执行”。await 之后的代码应按异步续体理解。

async 函数中的返回和异常

async 函数中:

Future<int> succeed() async {
  return 10;
}

Future<int> fail() async {
  throw StateError('失败');
}

return 10 会使返回的 Future 成功完成;throw 会使返回的 Future 失败完成。调用方如果不等待或监听错误,就可能产生未处理异步错误。

Future<void> main() async {
  try {
    final value = await succeed();
    print(value);

    await fail();
  } on StateError catch (error, stackTrace) {
    print('捕获到:$error');
    print(stackTrace);
  }
}

try/catch 必须包住实际的 await,这样错误才会沿着当前异步调用链返回到捕获点。

await 的错误传播

await future 有两种结果:

  • Future 成功:表达式的值就是 Future 的结果;
  • Future 失败:await 位置抛出对应错误,控制流跳到匹配的 catch
Future<int> readValue() async {
  throw FormatException('数据格式不正确');
}

Future<void> run() async {
  try {
    final value = await readValue();
    print(value); // 不会执行
  } on FormatException catch (error) {
    print('格式错误:$error');
  } catch (error) {
    print('其他错误:$error');
  }
}

on FormatException 用于按类型筛选错误,catch (error, stackTrace) 可以同时获得错误对象和堆栈。生产代码通常应保留原始堆栈,因为错误类型本身不足以定位失败发生的位置。

thencatchError 和错误链

Future 链也会传播成功值和错误:

Future<void> main() {
  return Future<int>.value(5)
      .then((value) {
        return value * 2;
      })
      .then((value) {
        print(value); // 10
      })
      .catchError((error, stackTrace) {
        print('处理错误:$error');
      });
}

then 回调返回普通值时,链继续成功;返回 Future 时,链会等待该 Future,这叫作 Future 链的扁平化:

Future<int> step1() async => 1;
Future<int> step2(int value) async => value + 1;

Future<int> chain() {
  return step1().then((value) {
    return step2(value); // 返回 Future<int>,不会变成 Future<Future<int>>
  });
}

如果 then 回调抛出异常,后面的成功回调会被跳过,错误继续寻找后续的错误处理器。

不过,复杂分支使用 try/catch 往往更容易验证错误范围:

Future<void> loadPage() async {
  try {
    final user = await loadUser();
    final posts = await loadPosts(user.id);
    print(posts);
  } catch (error, stackTrace) {
    print('页面加载失败:$error');
    print(stackTrace);
  }
}

catchError 的回调返回值必须与 Future 链要求的类型兼容;否则错误处理器本身可能再次产生类型错误。使用 try/catch 可以减少这类返回类型陷阱。

错误处理不是取消操作

捕获错误只改变错误的传播方式,不会自动撤销已经开始的操作:

Future<void> example() async {
  try {
    await request();
  } catch (error) {
    print('请求失败');
  }
}

如果 request() 内部已经向服务器发送数据,外层 catch 不会把网络请求回滚。是否能取消,取决于具体 API 是否提供取消机制,例如 HTTP 客户端的取消令牌、流订阅的 cancel 或自定义的取消状态。

这一区分在超时场景尤其重要。

Zone:未处理异步错误的边界

Dart 的 Zone 可以为一段异步执行树提供上下文和错误处理边界。runZonedGuarded 常用于捕获没有被 Future 链显式处理的异步错误:

import 'dart:async';

void main() {
  runZonedGuarded(() {
    Future<void>.delayed(const Duration(milliseconds: 10), () {
      throw StateError('后台任务失败');
    });
  }, (error, stackTrace) {
    print('Zone 捕获:$error');
    print(stackTrace);
  });
}

这里的异常发生在稍后的异步回调中,已经不在 main 的同步 try/catch 调用栈内,但仍处于该 Zone 的异步上下文中,因此可以由 Zone 错误处理器接收。

应区分三类问题:

  1. 同步异常:当前调用栈中的 try/catch 可以捕获;
  2. Future 失败:通过 awaitthen 或错误处理器捕获;
  3. 未处理异步错误:可能进入 Zone 的错误处理器,或由运行时报告。

在 Flutter 中,框架错误报告机制还包括 Flutter 自己的错误回调;Zone 不能替代对 Flutter 框架错误、平台通道错误和业务 Future 错误的分别处理。

超时:Future.timeout 做了什么

Future.timeout 为“等待结果”设置一个时间上限:

Future<String> requestWithTimeout() {
  return request().timeout(
    const Duration(seconds: 3),
    onTimeout: () => '使用缓存',
  );
}

它的语义是:

  • 如果原 Future 在 3 秒内成功或失败,使用原结果;
  • 如果 3 秒先到,且没有提供 onTimeout,返回的 Future 失败,通常是 TimeoutException
  • 如果提供 onTimeout,可以返回替代值或另一个 Future。
Future<String> request() async {
  await Future<void>.delayed(const Duration(seconds: 5));
  return '服务器结果';
}

Future<void> main() async {
  final result = await request().timeout(
    const Duration(seconds: 1),
    onTimeout: () => '超时降级结果',
  );

  print(result); // 超时降级结果
}

超时不会取消底层 Future

上例在 1 秒时返回了“超时降级结果”,但 request() 本身仍可能在 5 秒后完成。timeout 只是改变外部等待者看到的 Future 结果,不会自动停止原始操作。

可以用时间关系表示:

原操作开始       deadline                  原操作完成
    │──────────────│──────────────────────────│
    │              │                          │
    │        timeout Future 完成              │
    │        返回降级值或 TimeoutException      │
    │                                         │
    └──────底层操作仍可能继续执行──────────────┘

若原操作完成时间为 Toperation,超时期限为 Tdeadline

  • Toperation < Tdeadline:观察者得到原操作结果;
  • Toperation >= Tdeadline:观察者得到超时结果;
  • 但无论哪种情况,底层操作是否停止由底层 API 决定。

因此,超时后的副作用必须谨慎处理。例如请求已经发出时,不能因为客户端超时就假定服务器没有执行。涉及写操作时,应使用幂等键、状态查询或服务端去重机制。

Future 的并发组合

创建多个 Future 通常意味着多个操作可以同时进行,但“同时”要根据操作性质理解:

  • 网络请求可以在等待 I/O 时交错进行;
  • 文件和数据库异步操作可以重叠等待;
  • CPU 密集型 Dart 代码不会因为返回 Future 就并行执行;
  • 多个任务都在同一个 isolate 中时,仍然不会同时执行 Dart 指令。

顺序执行:后一个依赖前一个

Future<void> sequential() async {
  final user = await loadUser();
  final posts = await loadPosts(user.id);
  print(posts);
}

第二个请求必须使用第一个请求的结果,因此顺序是数据依赖导致的,而不是 await 本身“擅自限制了所有并发”。

独立操作:先启动,再等待

如果两个操作相互独立,可以先保存 Future:

Future<void> parallel() async {
  final userFuture = loadUser();
  final settingsFuture = loadSettings();

  final user = await userFuture;
  final settings = await settingsFuture;

  print('$user / $settings');
}

loadUser()loadSettings() 在各自被调用时就开始执行;第二个 await 不会等到第一个 await 完成后才启动设置加载。

把代码写成下面这样,语义不同:

Future<void> notParallel() async {
  final user = await loadUser();
  final settings = await loadSettings();

  print('$user / $settings');
}

这里 loadSettings() 只有在 loadUser() 完成后才被调用。

Future.wait:等待全部任务

Future.wait 用于把多个 Future 组合为一个 Future:

Future<void> loadDashboard() async {
  final results = await Future.wait<Object>([
    loadUser(),
    loadSettings(),
    loadNotifications(),
  ]);

  final user = results[0];
  final settings = results[1];
  final notifications = results[2];

  print('$user, $settings, $notifications');
}

它有三个重要性质:

  1. 返回的 Future 只有在所有输入 Future 成功后才成功;
  2. 返回列表的顺序与输入 Future 的顺序一致,不取决于完成先后;
  3. 任意输入失败时,组合 Future 会失败。

例如三个任务的完成顺序为:

输入顺序:      A        B        C
完成顺序:      B        C        A
结果列表:      [A结果, B结果, C结果]

Future.wait 默认不是遇到第一个错误就立即向调用方完成;eagerError: true 可以让组合 Future 在第一个错误出现时尽快失败:

final values = await Future.wait<String>(
  [
    loadPrimary(),
    loadSecondary(),
  ],
  eagerError: true,
);

eagerError: true 仍然不会取消其他 Future。它只改变“组合结果何时向外报告错误”,不能停止已经开始的任务。

cleanUp 可用于在整体失败时处理已经成功返回、但最终不会交给调用方的资源:

final files = await Future.wait<FileHandle>(
  [
    openFileA(),
    openFileB(),
  ],
  cleanUp: (file) {
    file.close();
  },
);

资源释放函数本身不应再抛错;否则可能造成额外的未处理异步错误。是否使用 cleanUp 取决于结果是否需要关闭、删除或归还,普通值不需要人为清理。

Future.wait 的失败路径

假设:

A:成功,返回资源 a
B:失败
C:仍在执行

Future.wait([A, B, C]) 失败时:

  • A 的结果不会作为整体结果返回;
  • B 的错误成为组合失败;
  • C 是否继续执行,取决于它自身是否可取消;
  • 组合失败不等于所有任务都已经停止。

因此,批量任务需要分别设计“错误传播”和“资源回收”,不能仅依赖 try/catch

Future.any:第一个完成者获胜

Future.any 返回第一个完成的 Future 的结果,无论这个结果是成功还是错误:

Future<String> fastestSource() {
  return Future.any<String>([
    loadFromCache(),
    loadFromLocalDatabase(),
    loadFromNetwork(),
  ]);
}

如果缓存先成功,调用方得到缓存;如果网络请求先失败,而其他来源稍后才成功,Future.any 也会先以错误结束。

这和“第一个成功者”不同。若需求是“忽略失败,直到某个来源成功”,不能直接使用 Future.any,需要为每个任务单独捕获错误,并定义全部失败时的结果:

Future<String> firstSuccessful(
  List<Future<String> Function()> factories,
) async {
  final errors = <Object>[];

  for (final factory in factories) {
    try {
      return await factory();
    } catch (error) {
      errors.add(error);
    }
  }

  throw StateError('所有来源都失败:$errors');
}

上面的实现是顺序尝试,不是并发尝试。并发“第一个成功者”需要同时启动所有任务,并维护成功计数和全部失败状态;还必须考虑失败任务和胜出后剩余任务的取消问题。Dart 核心 Future 没有通用的自动取消协议,因此这通常需要依赖具体库或自定义取消机制。

顺序批处理和并发批处理

下面两个实现处理同一批 ID,但并发行为不同:

Future<void> processSequentially(List<int> ids) async {
  for (final id in ids) {
    await process(id);
  }
}

Future<void> processConcurrently(List<int> ids) async {
  await Future.wait(ids.map(process));
}

顺序版本的总等待时间近似为:

Tseriali=1nTiT_{\text{serial}} \approx \sum_{i=1}^{n} T_i

其中 T_i 是第 i 个任务的耗时。

并发版本在资源允许时近似为:

Tparallelmax(T1,T2,,Tn)T_{\text{parallel}} \approx \max(T_1, T_2, \ldots, T_n)

但这个公式只描述理想等待时间,实际还会受连接池、服务器限流、内存、CPU 和任务调度影响。并发启动几千个网络请求可能比适度并发更慢,还可能触发服务端拒绝或客户端资源耗尽。

一个简单的并发上限实现如下:

Future<List<R>> mapWithLimit<T, R>(
  Iterable<T> items,
  int limit,
  Future<R> Function(T item) operation,
) async {
  if (limit <= 0) {
    throw ArgumentError.value(limit, 'limit', '必须大于 0');
  }

  final values = items.toList();
  final results = List<R?>.filled(values.length, null);
  var nextIndex = 0;

  Future<void> worker() async {
    while (true) {
      final index = nextIndex++;
      if (index >= values.length) {
        return;
      }

      results[index] = await operation(values[index]);
    }
  }

  final workerCount = limit < values.length ? limit : values.length;
  await Future.wait(List.generate(workerCount, (_) => worker()));

  return results.cast<R>();
}

这里有两个关键点:

  • nextIndex++ 在同一个 isolate 的同步代码中执行,不会被另一个 Dart 线程同时打断;
  • results[index] 使用输入索引写入,因此结果顺序仍与输入顺序一致;
  • 任意一个 operation 失败时,Future.wait 会失败,但已经启动的其他任务仍可能继续。

这个示例适用于控制异步操作数量,不适用于把 CPU 密集型工作变成并行计算。

Future.waitFuture.any 和超时的组合

下面是一个带整体超时的批量加载:

Future<List<String>> loadAll() async {
  return Future.wait<String>([
    loadA(),
    loadB(),
    loadC(),
  ]).timeout(
    const Duration(seconds: 5),
  );
}

其失败路径至少有三种:

  1. 所有任务在 5 秒内完成:返回完整列表;
  2. 某个任务先失败:整体 Future 失败;
  3. 5 秒先到:整体 Future 超时,但未完成的底层任务可能继续执行。

如果业务需要“部分成功”,就不能直接把全部任务交给 Future.wait,而应把每项结果封装成成功或失败状态:

sealed class Result<T> {
  const Result();
}

final class Success<T> extends Result<T> {
  const Success(this.value);
  final T value;
}

final class Failure<T> extends Result<T> {
  const Failure(this.error, this.stackTrace);
  final Object error;
  final StackTrace stackTrace;
}

Future<Result<T>> capture<T>(Future<T> future) async {
  try {
    return Success<T>(await future);
  } catch (error, stackTrace) {
    return Failure<T>(error, stackTrace);
  }
}

Future<List<Result<String>>> loadPartially() {
  return Future.wait<Result<String>>([
    capture(loadA()),
    capture(loadB()),
    capture(loadC()),
  ]);
}

这样,组合 Future 本身通常成功完成,但每个元素分别描述自己的成功或失败。它适合仪表盘、首页多个独立模块等场景;对于必须全部成功的事务,不应把失败转换成普通结果后悄悄忽略。

常见误解及其失败表现

Future 当作后台线程

Future<void> doHeavyWork() async {
  for (var i = 0; i < 1000000000; i++) {
    // CPU 密集型计算
  }
}

async 不会创建线程。调用它可能让 UI 卡顿,因为循环在当前 isolate 内同步执行。诊断时可以观察 Flutter DevTools 的帧耗时和 CPU profile;如果主 isolate 在长时间运行单段同步代码,问题不是缺少 await,而是需要拆分工作、优化算法或迁移到 isolate。

Future.delayed 模拟取消

await Future.delayed(const Duration(seconds: 1));

这个 Future 没有取消接口。即使调用方不再等待,它也可能继续完成。Future.delayed 适合延迟重试、演示和定时流程,不是通用取消工具。

以为超时后网络请求已经停止

try {
  await sendPayment().timeout(const Duration(seconds: 3));
} on TimeoutException {
  print('客户端超时');
}

这里只能证明“客户端等待超过 3 秒”,不能证明服务器没有收到或没有处理支付请求。支付、下单、上传等副作用操作必须设计幂等和查询机制。

误用 Future.wait 获得并发

for (final id in ids) {
  await load(id);
}

这段代码不是并发,因为下一次 load 的调用发生在上一次 await 成功之后。需要并发时,应先创建 Future:

final futures = ids.map(load).toList();
final values = await Future.wait(futures);

但这会一次性启动所有操作。数据量大时,应使用并发上限,而不是盲目扩大并发度。

在循环中递归安排微任务

这种代码可能导致定时器、I/O 和 Flutter 帧事件无法及时处理:

void keepScheduling() {
  scheduleMicrotask(keepScheduling);
}

如果需要让出事件循环,应重新设计任务边界,必要时使用事件队列或拆分工作;如果目标是避免 UI 阻塞,仅仅切换到微任务并不能解决 CPU 占用问题。

生命周期和平台边界

Future 没有统一的“页面生命周期感知”能力。一个页面销毁后,它发起的 Future 仍可能完成。Flutter 代码需要在结果返回时确认状态对象仍然有效:

Future<void> loadState() async {
  final data = await repository.load();

  if (!mounted) {
    return;
  }

  setState(() {
    // 使用 data 更新界面
  });
}

这里的 mountedState 的生命周期状态,不是 Future 的取消机制。它只能避免在已卸载的 widget 上更新 UI,不能阻止网络、文件或数据库操作继续执行。

在 Android、iOS 和桌面平台,应用进入后台可能被暂停、限制甚至直接终止;挂起期间的定时器和 Future 不提供“必定按墙上时间完成”的保证。Web 还可能受到浏览器后台标签页限速。需要可靠完成的任务,应交给平台后台任务机制或服务端,而不是只依赖当前 Dart isolate 中的 Future。

Dart VM 和 Flutter 原生平台通常可以使用 isolate 执行隔离计算;Flutter Web 的 isolate 能力、线程模型和可用 API 受浏览器环境限制。不能仅因为代码在移动端可通过 isolate 并行执行,就假设同一代码在 Web 上拥有相同的线程行为。

诊断异步问题的路径

遇到 Future 相关问题时,可以按执行路径检查,而不是只在末端打印“加载失败”:

  1. 确认操作何时启动:是调用函数时启动,还是第一次 await 后才启动;
  2. 确认是否阻塞当前 isolate:查找长循环、同步文件操作、复杂 JSON 解析和大对象转换;
  3. 记录开始、成功、失败和超时时间:区分底层操作慢、组合等待慢和 UI 更新晚;
  4. 保留错误堆栈:使用 catch (error, stackTrace),不要只记录 error.toString()
  5. 检查错误是否被吞掉catchError 是否返回了错误类型不兼容的值,是否把失败转换成了无意义的默认值;
  6. 检查超时后的副作用:底层 Future 是否仍在运行,是否可能重复提交;
  7. 检查生命周期:Future 完成时,页面、State 或请求上下文是否仍然有效;
  8. 检查并发数量Future.wait 是否一次启动了过多任务,服务端和客户端是否支持这样的负载。

可以给异步操作附加上下文:

Future<T> traced<T>(
  String name,
  Future<T> Function() operation,
) async {
  final started = DateTime.now();

  try {
    final value = await operation();
    print('$name succeeded: ${DateTime.now().difference(started)}');
    return value;
  } catch (error, stackTrace) {
    print('$name failed: $error');
    print(stackTrace);
    rethrow;
  }
}

rethrow 会保持当前错误的传播方式和原始堆栈语义;重新 throw error 可能改变堆栈起点,不适合无必要地替换原始错误。

核心边界

Future 解决的是“一次异步结果如何表示和组合”,不是所有并发问题的统一答案:

  • 它不自动提供线程;
  • 它不自动取消底层操作;
  • 它不保证精确的时间;
  • 它不保证应用生命周期跨越后台或进程终止;
  • 它不替代资源释放、幂等设计和服务端事务;
  • 它不让 CPU 密集型同步代码变得非阻塞。

正确的推导顺序是:先确定任务是在当前 isolate 中同步执行、异步等待 I/O,还是需要另一个 isolate;再确定 Future 的成功和失败边界;随后选择顺序等待、Future.waitFuture.any 或带并发上限的组合;最后单独设计超时、取消、生命周期和副作用处理。只有把这些边界分开,async/await 才能准确表达程序的数据依赖和故障路径。


系列导航与关联阅读

官方资料

本文依据 Flutter 与 Dart 官方文档重新梳理;正文与示例由 WR BLOG 编写。