throws_plugin
Analyzer plugin for @Throws/@throws usage.
Install
Add the plugin dependency to your pubspec.yaml and enable it in analysis_options.yaml.
analysis_options.yaml:
plugins:
throws_plugin:
version: any
What the plugin enforces
The plugin analyzes resolved AST (except files under test/,
integration_test/ and test_driver/ of a package, where the test framework
catches errors) and:
- Requires @Throws/@throws on functions that throw.
- Requires callers to handle throwing calls with try/catch or declare errors.
- Validates that declared errors match actual thrown errors.
- Accounts for overrides by walking the override chain.
Lints
- missing_throws_annotation: functions that throw must be annotated with @Throws/@throws
- no_try_catch: calling a throwing function without a try/catch or annotation
- no_try_catch_in_closure: calling a throwing function in a closure whose
errors nothing outside can catch (not passed to a
@rethrowsfunction) - not_exhaustive_try_catch: try/catch does not cover all expected errors
- throws_annotation_mismatch: @Throws errors do not match actual thrown errors
- introduced_throws_in_override: overrides introduce new errors without annotation
- unused_throws_annotation: @Throws on functions that do not throw
Diagnostics are reported at the call site for unhandled throws.
Assists and fixes
Assists:
- Add
@Throws()or@throwsto a function. - Specify errors: turn
@throws/@Throws()into@Throws(errors: {...})with the errors the function throws.
Fixes:
missing_throws_annotation: add@Throwswith the inferred errors.throws_annotation_mismatch: update@Throwserrors.unused_throws_annotation: remove the annotation.no_try_catch:- wrap the statement in try/catch, with a clause per error or one
on Objectclause (final x = call();becomeslate final T x; try { x = call(); },void f() => call();becomes a block body); - add
awaitto a future-returning call inside try/catch; - add the errors to the enclosing
@Throws, or annotate the enclosing function.
- wrap the statement in try/catch, with a clause per error or one
no_try_catch_in_closure: wrap the statement (or avoid=>body) in try/catch inside the closure, or addawait.not_exhaustive_try_catch: add the missing catch clauses (subtypes before supertypes), a defaulton Objectclause, or add the errors to@Throws.introduced_throws_in_override: add the errors to@Throwsof the overridden member (when it is in your package), or wrap the call in try/catch.
Generated code selects the reason and the catch bodies as linked edits.
The annotation fixes for missing_throws_annotation,
throws_annotation_mismatch and unused_throws_annotation can be applied to
every diagnostic in a file at once ("fix all in file" in the IDE). dart fix
does not apply analyzer plugin fixes.
throws.yaml
Place throws.yaml at the package root to declare errors of members outside your package (SDK or dependencies).
throws:
# Errors to check: exception (subtypes of Exception), error (subtypes of
# Error), or both (default). Types that are neither are always checked.
track: [exception, error]
# Bundled maps: dart, flutter. Off by default.
prebuilt: [dart]
# YAML files, or directories of them, in the same format as `members`
# (e.g. throws_collector output; its `dependencies` input writes a map
# per package in pubspec.lock).
include:
- throws_collector_gen/
# Library URI, then member: errors (a name or a list).
members:
dart:core:
Iterable.single: [StateError]
int.parse: FormatException
package:http/http.dart:
get: [ClientException]
Client.new: [ArgumentError]
# Library URI, then members that throw what their callbacks throw, like
# `@rethrows`.
rethrows:
package:retry/retry.dart: [retry, RetryOptions.retry]
Member names follow Dart tear-off syntax: function for top-level functions,
Class.member for methods, getters and setters, Class.new and
Class.named for constructors.
Notes:
- Prebuilt maps are heuristic only and come with no guarantees of completeness or accuracy.
- Entries from
membersandincludetake precedence over prebuilt maps. prebuilt: [dart]also marks SDK higher-order members as rethrowing:Iterable.forEach/map/where/...,List.sort,Map.putIfAbsent,Future.then/catchError/whenComplete,Future.new/sync/microtask,runZonedand others.- Unknown keys are ignored. Version 1 keys (
include_prebuilt,include_paths,custom_errors,include_sdk_errors) are no longer read.
How errors are resolved
When evaluating a call, the plugin resolves expected errors in this order:
- @Throws/@throws on the target element.
- throws.yaml (
membersandinclude). - Prebuilt maps (dart/flutter) if enabled.
- Overrides (walks the override chain until a throwing element is found).
If none are found, the call is treated as non-throwing.
Errors are compared as types, including subtypes: on Exception handles a
call that throws FormatException, and @Throws(errors: {Exception}) covers a
thrown FormatException. Names from throws.yaml and prebuilt maps are resolved
in the scope of the library that declares the member; unresolved names count as
an unknown error, which any catch clause handles.
Closures and @rethrows
A closure is checked as a function of its own. What happens to its errors depends on where it goes:
-
passed to a parameter of a function annotated with
@rethrows(or listed underrethrows), its errors are thrown by that call, so the caller handles or declares them:@rethrows T measure<T>(T Function() action) => action(); @Throws(errors: {FormatException}) int parse(String s) => measure(() => int.parse(s)); -
anywhere else (
onPressed: () => save(),Timer(d, () => ...), a variable), nothing outside can catch them:no_try_catch_in_closurereports throwing calls inside it. An explicitthrowin a closure is not reported.
A tear-off of a known function (list.forEach(parse)) is treated like a closure
that calls it. An async closure passed to a parameter that returns void
(forEach((x) async => ...)) loses its errors in a dropped future, so its errors
stay inside it.
@rethrows is inherited by overrides and combines with @Throws for the
function's own errors. Lazy members (map, where, ...) throw when iterated,
but their callback's errors are reported at the call.
Futures and streams
Errors of a call that returns a Future arrive asynchronously:
- a surrounding try/catch handles them only if the call is awaited
(
await f(),await Future.wait([f()])); .catchError,.ignore()and.then(..., onError: ...)handle all of them,.onError<E>handles subtypes ofE.
Errors of a call that returns a Stream arrive as error events:
- a surrounding try/catch handles them only through
await for (x in s()); .handleErrorand.listen(..., onError: ...)handle all of them;.listenwithoutonErrorleaves them unhandled;- getters and methods that return a future (
await s().first) follow the future rules;yield* s()forwards them to the caller.
A future or stream stored in a local variable (final f = load();) is
handled where the variable is read: await f inside try/catch,
f.catchError(...), await for (x in f). Diagnostics point at that read. If no
read awaits or handles it, or the variable is reassigned or read in a closure,
the call itself is reported. Fields and other non-local storage are not tracked.
Performance
Measured on Flutter DevTools (packages/devtools_app: 487 files, 115k lines in
lib), Apple M4 Max, Flutter 3.49 master, prebuilt: [dart, flutter].
dart analyze, mean of 3 runs:
| Setup | Cold cache | Warm cache |
|---|---|---|
| No plugin | 22.3 s | 4.3 s |
| Empty analyzer plugin (no rules) | 45.0 s | 24.6 s |
| throws_plugin | 46.4 s | 25.2 s |
Almost all of the extra time comes from loading any analyzer plugin: the
plugin isolate analyzes the project again. throws_plugin itself takes 120 ms
for all 487 files (warm JIT; p95 0.8 ms, max 2.4 ms per file) next to 13.6 s of
resolution. Measure your project with dart run tool/benchmark.dart <dir>.
In the IDE (the same analysis server over LSP, warm cache, one run each):
| Setup | First diagnostics | Memory (settled / peak) | Edit to diagnostics |
|---|---|---|---|
| No plugin | 4.4 s | 1.3 / 1.3 GB | 307 ms |
| Empty analyzer plugin | 36.5 s | 4.2 / 4.8 GB | 328 ms |
| throws_plugin | 25.0 s | 4.2 / 4.7 GB | 319 ms |
Any analyzer plugin costs about 3 GB of memory and 20–30 s before the first diagnostics on a project of this size. Diagnostics after an edit arrive 10–20 ms later than without a plugin.
Limitations
-
Casts (
as) and null checks (!) are not treated as throwing: they throwTypeError, a programming error. -
A
catchthat mayrethrow(even conditionally) does not count as handling. -
Function types carry no errors: a callback stored in a variable or field, or passed through an unannotated parameter, loses them. A
@rethrowsfunction rethrows all errors of its arguments, even those it catches itself. -
Analysis is best-effort and depends on available type information.
-
SDK/dependency coverage depends on map data (prebuilt or collected).