Skip to content

avoid_exit_outside_entrypoint

v1.1.0WarningConfigurableCode Quality

Warns when dart:io’s exit() is called outside the program’s entrypoint. By default only bin/** is allowed.

exit() in domain code destroys testability outright: the test process disappears mid-assertion. There is nothing to catch, nothing to assert on, and the failure mode is a runner that reports nothing rather than a red test — which reads as a passing suite far more often than it should.

The split that fixes it is small. Domain code throws a typed error, and one thin entrypoint maps errors onto exit codes. That is also what makes the whole error taxonomy testable: you assert on the thrown type, and the mapping lives in one file you can read top to bottom.

banned_usage matches Type.member or a bare member name. exit is a top-level function from dart:io, not a member of a type, and a bare exit entry would also hit any method of your own with that name. More importantly the useful policy here is path-based — allowed in the entrypoint, banned everywhere else — which is rule-shaped rather than project vocabulary. This rule resolves the element, so Terminal().exit(3) and a local function named exit are never reported.

See also: dart:io exit | Writing command-line apps

lib/src/core/upload.dart
import 'dart:io';
Future<void> upload(File artifact) async {
final response = await client.put(artifact);
if (response.statusCode == 403) {
stderr.writeln('Permission denied');
exit(3);
}
}

A test of upload() cannot assert on that 403 branch — reaching it kills the test runner.

lib/src/core/upload.dart
Future<void> upload(File artifact) async {
final response = await client.put(artifact);
if (response.statusCode == 403) {
throw const AuthFailure('Permission denied');
}
}
// bin/tool.dart — the one place that knows about exit codes.
Future<void> main(List<String> args) async {
try {
await upload(File(args.first));
} on AuthFailure catch (e) {
stderr.writeln(e.message);
exit(3);
}
}

The branch is now testable:

test('a 403 is an auth failure', () {
expect(() => upload(artifact), throwsA(isA<AuthFailure>()));
});

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 avoid_exit_outside_entrypoint: true.

To turn it off:

many_lints.yaml
rules:
avoid_exit_outside_entrypoint: false

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

analysis_options.yaml
many_lints:
rules:
avoid_exit_outside_entrypoint:
allow_in:
- 'bin/**'
- 'tool/**'
Option Type Default Description
allow_in list of globs ['bin/**'] Paths where exit() is permitted. Replaces the default
additional_allow_in list of globs [] Adds to whichever list won, without restating the default

Globs are matched against the path relative to the package root, with / as the separator on every platform.