Skip to content

avoid_passing_async_when_sync_expected

v1.0.0WarningConfigurableAsync Safety

This rule flags an async closure passed to a parameter typed void Function(...). The returned Future is assigned to void and dropped: nothing awaits the work, and any error inside it escapes as an unhandled async error.

void is the one return type that both accepts an async closure and silently discards what it produces. The call site compiles, the callback runs, and the caller moves on immediately — before the awaited work has finished.

The more damaging half is error handling. An exception thrown inside a dropped future never reaches the caller’s try/catch; it surfaces as an unhandled async error, usually as console noise far from the code that caused it.

This is deliberately the only shape reported. For any other return type the analyzer already rejects the argument — Future<int> Function() is not assignable to int Function() — so there is nothing left for a lint to add. dynamic and Object returns accept the future as a value the callee can still store or await, so they are not reported either.

See also: Dart: asynchronous programming, unawaited_futures

A retry hook typed void Function(). The async body compiles, runs, and its failure has nowhere to go:

void onRetry(void Function() retry) => retry();
void wire(Future<void> Function() save) {
onRetry(() async {
await save(); // nothing awaits this; a failure becomes unhandled
});
}

Change the parameter so the future has somewhere to go:

Future<void> onRetry(Future<void> Function() retry) => retry();
Future<void> wire(Future<void> Function() save) async {
await onRetry(() async {
await save();
});
}

Third-party APIs are often fixed. Handle the error inside the closure so nothing escapes into the zone:

void wire(void Function() onRetry, Future<void> Function() save) {
onRetry(() async {
try {
await save();
} catch (error, stackTrace) {
_reportError(error, stackTrace);
}
});
}

onPressed and friends are void-returning by design, and giving them an async body is idiomatic Flutter. They are skipped, so this is not reported:

ElevatedButton(
onPressed: () async {
await _save();
},
child: const Text('Save'),
)

The skipped names are onPressed, onTap, onLongPress, onChanged, onSubmitted, onDoubleTap, onRefresh, onSaved and listener. With ignore_widget_callbacks: false those are audited too — worth doing only if your app installs an error boundary that catches what they drop.

Only arguments are checked. A closure assigned to a variable of type void Function() is not reported — the same hazard exists, but the assignment is explicit enough that the author chose the type deliberately.

Flutter’s onPressed, onTap and similar handlers are void-returning by design and are idiomatically given async bodies. They are ignored by default. Set ignore_widget_callbacks: false if your project has an error boundary for those futures and wants to audit every fire-and-forget handler.

This rule is in the opinionated preset, so it is on with preset: opinionated, or by name:

many_lints.yaml
rules:
avoid_passing_async_when_sync_expected: true

To turn it off again:

many_lints.yaml
rules:
avoid_passing_async_when_sync_expected: false

To keep the rule on but skip certain paths, use per-rule exclude.

analysis_options.yaml
many_lints:
rules:
avoid_passing_async_when_sync_expected:
ignore_widget_callbacks: true
ignored_parameters: [onRetry]
Option Type Default Description
ignore_widget_callbacks bool true Skip the common Flutter handlers (onPressed, onTap, onLongPress, onChanged, onSubmitted, onDoubleTap, onRefresh, onSaved, listener)
ignored_parameters list of strings [] Additional parameter names to skip, by name