Skip to content

prefer_typed_exceptions

v1.1.0WarningConfigurableControl Flow

Warns when a throw does not name a type a caller could catch selectively — a bare Exception('...'), a raw string, or any object that is neither Error nor Exception.

throw Exception('upload failed') cannot be caught by kind. Every catch downstream either swallows everything or re-inspects the message string, and message matching breaks the moment somebody improves the wording — silently, because the catch still compiles and still runs.

Three things are reported:

  • throw Exception(...) and throw Error(...) — the base types, which say nothing.
  • throw 'a string' (or any non-throwable value).
  • throw SomeClass(...) where SomeClass implements neither Error nor Exception.

An SDK type from the allow list is accepted, and so is any project type that implements Exception or Error.

Dart’s own only_throw_errors covers the adjacent case — throwing a bare string — but is satisfied by Exception(...), which is where most of the damage is. This rule covers both, so enabling it alone is sufficient.

See also: Dart lint: only_throw_errors | Effective Dart: error handling

class Failure {
const Failure(this.message);
final String message;
}
void uploadArtifact() {
// Nothing downstream can catch just this failure.
throw Exception('upload failed');
}
void uploadRaw() {
// Not even an Exception.
throw 'upload failed';
}
void uploadDomain() {
// A domain class that forgot `implements Exception` is just as opaque:
// `on Failure` does not compile against a non-throwable.
throw const Failure('upload failed');
}

Give each failure a type, and the boundary can branch on it:

class UploadFailure implements Exception {
const UploadFailure(this.path);
final String path;
}
class AuthFailure implements Exception {
const AuthFailure();
}
void uploadArtifact(String path) {
throw UploadFailure(path);
}
Future<void> run(String path) async {
try {
await upload(path);
} on AuthFailure {
exitCode = 3;
} on UploadFailure catch (failure) {
stderr.writeln('could not upload ${failure.path}');
exitCode = 4;
}
}

SDK types whose own name already identifies the failure are accepted as they are:

void configure(int port) {
if (port <= 0) {
throw ArgumentError.value(port, 'port', 'must be positive');
}
}

This rule is in the recommended preset, so it is on with preset: recommended, preset: opinionated or preset: pedantic. Add it to preset: core with prefer_typed_exceptions: true.

To turn it off:

many_lints.yaml
rules:
prefer_typed_exceptions: false

To keep the rule on but skip certain paths — tests often throw freely — use per-rule exclude.

Both options name dart:core types only. A type from any other library — TimeoutException, or one of your own — is judged by whether it implements Exception or Error, which no list can override.

Extends the default list. Use it to accept a core type the defaults leave out:

analysis_options.yaml
many_lints:
rules:
prefer_typed_exceptions:
additional_allow:
- OutOfMemoryError

Replaces the default list outright. Use it to narrow the defaults — a project that wants every failure named, ArgumentError included, can empty the list:

many_lints.yaml
rules:
prefer_typed_exceptions:
allow: []

Prefer additional_allow for anything else: a restated default list silently rots when a later version of this package adds a name.

Option Type Default Description
allow list of strings see below dart:core types specific enough to throw directly. Replaces the default list
additional_allow list of strings [] Adds to whichever list won, so you can extend the defaults without restating them

The default allow list is:

ArgumentError, AssertionError, ConcurrentModificationError, FormatException, IndexError, RangeError, StateError, UnimplementedError, UnsupportedError.

Each of these is already as catchable as a hand-written class would be, so wrapping one adds a type without adding information.