skip to content

In a Dart command-line program, why is assigning dart:io's top-level exitCode usually safer than calling exit(1) to report failure?

level: middleimportance: nice to knowfreq 22%

answer

  1. immediate termination versus normal end
  2. finally blocks and pending futures
  3. last assignment wins, default 0
  4. stdout is an IOSink
  5. keep codes within 0 to 127

basics

~20 s

exit() ends the Dart VM immediately without waiting for asynchronous work or running finally blocks, so buffered output and pending writes can be lost. Setting exitCode records the status and lets the program finish normally, flushing and closing first.

solid answer

~40 s

`exit(code)` returns `Never`: it stops the VM at once, does not wait for pending async operations and does not run `finally` blocks, and the docs warn it is very likely to lose data. Child processes are not explicitly terminated either. `exitCode` is a top-level setter in `dart:io`; it defaults to `0`, the last assignment from any isolate wins, and it takes effect when the program ends normally - after your `IOSink`s, files and sockets have been closed. So a CLI writes errors to `stderr`, sets `exitCode = 1`, and returns from `main`. Two more details: the Dart executable itself uses 254 for compile-time errors and 255 for an unhandled exception, so the docs recommend codes in 0 to 127; and `stdin` is a `Stream<List<int>>` with `readLineSync()` returning `null` at end of input.

code

dart · 16 lines
dart
import 'dart:io';

Future<void> main(List<String> args) async {
  if (args.isEmpty) {
    stderr.writeln('usage: lint <file>');
    exitCode = 64;
    return;
  }
  try {
    final text = await File(args.first).readAsString();
    stdout.writeln('${text.split('\n').length} lines');
  } on FileSystemException catch (e) {
    stderr.writeln('cannot read ${args.first}: ${e.message}');
    exitCode = 1;
  }
}

go deeper

for a junior

Recall that stderr is for errors and that a non-zero exit status signals failure to the shell.

for a middle

Explain what exit() skips, how exitCode applies at normal termination, and which code ranges are safe.

for a senior

Structure a CLI so every failure path sets a meaningful exitCode, closes its sinks and never loses output.

for a principal

Define exit-code conventions shared across a team's tools so scripts and CI can react to them reliably.

## Standard streams in a Dart CLI A Dart command-line program talks to its terminal through three top-level getters in `dart:io`: - **`stdin`** is a `Stdin`, which is a `Stream<List<int>>`. You either listen to it as a byte stream (decoding with `utf8.decoder` and splitting lines) or call `stdin.readLineSync()`, which returns the next line as a `String?` - `null` when input has ended - and blocks the isolate while waiting. - **`stdout`** and **`stderr`** are `Stdout` objects that implement `IOSink`: `write`, `writeln`, `add`, `addStream`, `flush`, `close`. Normal output goes to `stdout`; diagnostics go to `stderr` so they do not pollute piped output. - `stdin.hasTerminal` and `stdout.hasTerminal` tell you whether a real terminal is attached, which matters for colours or prompts. The last step of a CLI is its **exit status**, the number a shell or CI script checks. Dart gives two ways to set it. ## exit(): stop now `exit(int code)` is declared to return `Never`. Per its documentation it: 1. **exits the VM immediately** with the given code; 2. **does not wait** for any asynchronous operation to finish; 3. **does not execute `finally` blocks**; 4. is therefore **very likely to lose data** - for instance output still buffered in an `IOSink`, or a file write not yet flushed; 5. does not explicitly terminate child processes. Some embedders also forbid it: the implementation throws if the host has disabled `exit()`. ## exitCode: record and finish `exitCode` is a top-level setter and getter. Its value starts at **0**, is global to the VM, and **the last assignment from any isolate wins**. It is applied when the program terminates **normally** - when `main` has returned and no more work is pending. That gives your code the chance to `await` sink closes, finish writing reports and let `finally` blocks run. | | `exit(code)` | `exitCode = code` | |---|---|---| | When it takes effect | immediately | at normal termination | | `finally` blocks | skipped | run | | Pending async work | abandoned | completes first | | Typical use | fatal, unrecoverable state | reporting failure from `main` | ## Choosing exit codes On Linux and macOS, only the low 8 bits reach the shell, so `-1` shows up as `255`. The Dart executable itself reports **254** for compile-time errors and **255** for an unhandled runtime exception. To avoid clashing with those and with platform quirks, the docs recommend codes in the range **0 to 127**. Many tools use `64` for a usage error, following the Unix convention. ## A pattern that works - Parse arguments; on a usage error, write help to `stderr`, set `exitCode = 64` and `return`. - Do the work with `await`; on failure, write the reason to `stderr` and set `exitCode = 1`. - Close any files or sinks you opened, then let `main` return. - Reserve `exit()` for the rare case where the process must stop even though work is still pending, and flush what matters first. ## Common mistakes - Calling `exit(1)` right after `stdout.write(...)` and losing the message. - Expecting `finally` cleanup to run after `exit()`. - Returning an `int` from `main` expecting it to become the exit code; `main` does not use its return value that way. - Using large or negative codes that the OS truncates.

  • What does stdin.readLineSync() return when the input stream has ended?
    It returns `null` if end of input is reached before any bytes are read, which is why its type is `String?`. If some bytes precede end of input, it returns them without a line terminator. It blocks the isolate while waiting.
  • Which exit codes does the Dart executable itself use, and why does that matter?
    It reports 254 for compile-time errors and 255 for an unhandled runtime exception. If your tool also uses those codes, a caller cannot tell your deliberate failure from a crash, so the docs recommend staying in 0 to 127.

saying these in an interview costs you the question

  • Believes exit() runs finally blocks before terminating
  • Assumes exit() waits for pending writes to complete
  • Thinks the value returned from main becomes the exit code
  • Uses exit(-1) expecting the shell to see -1
  • Writes error messages to stdout instead of stderr