The exit status of a program must follow a few rules, and beyond these rules there are common conventions. None of these conventions are related to the low-level error that caused the program to exit. It's possible to write a program that exits with a certain error code it decided to exit because a file didn't exist, and a different error code if it decided to exit because it was denied permission to access a file, and a different error code if a directory component of a path turned out to be a non-directory, and so on, but that would be extremely unusual and difficult to arrange.
The exit status of a program is an integer value. On POSIX systems, the type of this value is
int, which usually ranges from -231 to 231+1. However most of this range is unusable in practice for several reasons. First and foremost, for historical reasons, most interfaces that allow a program to observe the exit status of its children only return the lower 8 bits of the exit status, which is a value between 0 and 255. This includes the system functions
waitpid¹ as well as the exit status in the shell². So for almost all intents and purposes, an exit status is an 8-bit value.
The value 0 is treated as a success and every other value is treated as a failure. This is the case in the shell, where boolean operators,
while constructs and anything else that involves a notion of true/false considers the exit status 0 to be true and any other status to be false. This is also the case with
make, where a nonzero exit status causes the build to stop with an error message and an error status. You can quibble whether it's a convention (since the author of a program technically can return whatever status it wants, and “success” and “failure” aren't defined formally anyway), but in practice, a program that exits with the status 0 is considered to have succeeded and a program that exits with another status (1–255) is considered to have failed.
A further feature of the shell specifically that limits the range is that the exit status in the shell (observed via
$?) encodes other information:
- 126 indicates that the command name is an existing file which is not executable.
- 127 indicates that the command name could not be found.
- 128+N traditionally (and still today in most shells) indicates that the command exited with signal N. A few shells use a different range, always beyond 128.
Therefore, in practice, programs cannot usefully use an exit status beyond 125. This leaves the values 1–125 to express different errors.
There is a somewhat widespread, but far from universal convention that larger values are treated as “worse” failures. In particular, for search commands such as
grep, 1 indicates “not found” and 2 or more indicates some error that prevented the search (e.g. file not found, as opposed to file found but does not contain the search string). Similarly, comparison commands such as
diff exit with the status 0 to mean “identical files”, 1 to mean “different files” and 2 or more to mean “the comparison could not be completed due to an error”.
A few programs define different error codes for different errors, for example sendmail and some other mail-related programs (values defined in
sysexits.h), rsync, curl, wget.
By far the most common convention for error codes is 0 for success, 1 for failure. The C and C++ programming languages define
EXIT_FAILURE as an exit status code to use to report a failure if there's no particular reason to choose a specific value, and
EXIT_FAILURE is 1 on most systems.
Errors such as “No such file or directory”, “Permission denied”, “Not a directory” and so on have a numerical encoding under the hood: they're
errno values, returned by system functions to indicate what went wrong. Errno values are generally not useful as exit status from programs. They encode the minutiae of what went wrong, rather than what it means for the specific program. For example wget's exit status distinguishes “parse error in options” (usually no underlying system error), “local input/output error” (regardless of the underlying system error), “network failure” (which would largely share the same system errors as local I/O), etc. Knowing whether wget failed due to a network error or to a local file error is more useful than knowing whether it failed due to a broken pipe (writing to a pipe, or closed connection on a network socket?) or to a permission error (unable to read the configuration file, or network access denied by a local policy?).
It's somewhat uncommon for return statuses to follow errno values. It does happen, especially with Perl scripts, due to the way Perl's
die function works. But it's a bad idea, not only because as I mentioned above the errno value is rarely the most useful part of the information, but mostly because there's no reason why errno values would be in the range 1–125. Fortunately I don't know of any system where errno values are outside the range 1–255, so at least
exit(errno) (or Perl's
die) won't exit with a value which is a multiple of 256, which as we saw above would convey success. But on Linux, for example, they do reach 126, and a program that exited with
errno == ERFKILL (“Operation not possible due to RF-kill”) would be undistinguishable from the shell from a program that died of SIGILL (illegal instruction).
waitid grants access to the full
int value via
$? or otherwise. For example, if
exit256 is a program that exits with
exit(256), the shell command
if exit256; then echo "exited with 0"; fi prints “exited with 0”.