throws_plugin 2.1.0
throws_plugin: ^2.1.0 copied to clipboard
Analyzer plugin for @Throws/@throws usage.
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).
