Skip to main content

Output and exit codes

5 min readStableIntermediate

What reserve prints, where it prints it, and the codes a script can branch on.

Exit codes

These are a contract and don't change meaning within a major version.

0exit code

The command succeeded. For a sweep, at least one name is available.

1exit code

The sweep ran and nothing is available. This covers both "every name is taken" and "some lookups couldn't be answered": read the unknown count from the tally, or from --json, to tell the two apart.

2exit code

The arguments were wrong.

4exit code

The network couldn't be reached, including a stall where every registry refused or went silent partway through. reserve serve also exits here when it can't bind the address it was given, or when the server it started stops answering.

5exit code

A file couldn't be written. This also covers the tool failing to start, such as a terminal it can't take over.

130exit code

The run was interrupted (Ctrl-C, or the equivalent signal on your platform).

101exit code

The tool panicked. This is a bug, and a backtrace follows when RUST_BACKTRACE=1 is set.

CONSOLE
CONSOLE
$ reserve your-domain-name --tld com,net,io,dev,com.bd,net.bd
...
4 available  2 taken  0 unknown   (6 checked)
$ echo $?
0
 
$ reserve example.com --tld com
   DOMAIN       STATUS  SOURCE      TIME  NOTE
-  example.com  TAKEN   registry  1104ms
0 available  1 taken  0 unknown   (1 checked)
$ echo $?
1
Bash
Bash
# a typical CI script branching on the exit code
if reserve "$1" --tld com,net --available-only --quiet; then
  echo "still free"
else
  echo "someone already registered it"
fi

Stable failure identifiers

Every failure also carries a stable identifier, printed as the last line of the error block on stderr: code: <identifier>. A script can match on this instead of parsing the error message.

Plain Text
error: <message>
  caused by: <cause>
  try: <remedy>
  code: <identifier>

Identifiers that mean the arguments were wrong (exit 2):

  • file.unreadable: the named file couldn't be read
  • catalog.malformed: the catalog data couldn't be parsed
  • catalog.version: a data file is stamped with a schema version this build doesn't read, so it's refused rather than loaded and misread
  • catalog.empty_selection: the filters given leave no extension to check
  • catalog.restricted_only: every match is one the public can't register under
  • group.unknown: the named group isn't a known group
  • extension.invalid: the given value isn't a usable domain extension
  • filter.invalid: the given value isn't usable for that setting
  • name.invalid: the given name isn't a usable domain name
  • name.list_empty: no name was given to check

Identifiers that mean the network failed (exit 4):

  • bootstrap.unavailable: the registry server list couldn't be fetched and no cached copy is usable
  • network.unreachable: no lookup reached a registry
  • registry.refused: every registry refused the question, which usually means this address is being throttled
  • registry.silent: every registry stopped answering part way through
  • server.bind_failed: reserve serve couldn't bind the address it was given
  • server.stopped: the bind succeeded and the server ran, then stopped answering

Identifiers that mean a file couldn't be written (exit 5):

  • output.unwritable: output couldn't be written to the named target

A stall report (network.unreachable, registry.refused, or registry.silent) looks like this on stderr:

Plain Text
network: no lookup reached a registry, so this machine may have no working connection
  code: network.unreachable

The diagnosis comes from the lookups the run already made, not a separate connectivity check: a pre-flight probe would race the real request and report success behind a captive portal, so there is none. A single answered lookup means the network is never blamed, whatever else failed.

stdout vs. stderr

Results go to stdout. Progress, warnings, prompts, and errors go to stderr, so a pipe receives only data. When stdout isn't a terminal, decoration is dropped and the output stays parseable. A closed pipe ends the run quietly rather than panicking.

On a terminal, a run opens with its start time and closes with its duration, both on stderr:

CONSOLE
CONSOLE
started 2026-08-20T00:34:22+06:00
+  your-domain-name.com.bd  AVAILABLE  text  803ms
finished 1 checked in 1.29s

The timestamp carries its offset, so a line pasted into an issue says which clock it came from.

A long sweep draws a one-line progress indicator on stderr, naming the domain in hand alongside the count:

CONSOLE
CONSOLE
checking 14 of 22  your-domain-name.shop 1s

It never touches stdout, and (along with the start/finish lines above) it stays away entirely when stderr isn't a terminal, in continuous integration, under --json or --no-input, or whenever colour is suppressed. -q/--quiet drops it too; --progress always or --progress never overrides the automatic decision either way.

Colour

Colour follows these rules, first match wins:

  1. An explicit --color always or --color never flag.
  2. NO_COLOR set to a non-empty value, or CLICOLOR=0: colour off.
  3. FORCE_COLOR (non-empty, not 0) or CLICOLOR_FORCE (anything but 0): colour on, even through a pipe.
  4. A dumb terminal (TERM unset, empty, or dumb): colour off.
  5. Otherwise, whether that stream is a terminal, decided separately for stdout and stderr, so a piped stdout can stay plain while an attached stderr keeps colour.

Log output takes the stderr half of that decision, so --color never, NO_COLOR, or a piped stderr strips the colour from -v logs the same way it strips it from everything else.

Colours come from the sixteen named terminal colours, so retheming your terminal rethemes the tool. Meaning is never carried by colour alone. A symbol or a word always carries it too.

Was this page helpful?

© 2026 Reserve. All rights reserved.