Exit Codes — Communicating Success and Failure
Every command exits with a numeric code: 0 means success, anything else is failure. Understanding and correctly using exit codes makes your scripts composable and automatable.
January 27, 20266 min read
BashExit CodesError HandlingScripting
Exit codes are the contract between a script and its caller — whether that's a human, another script, a CI pipeline, or systemd. Get them right and your script becomes a good citizen of the Unix toolchain.
The Convention
Exit code meanings
| Code | Meaning |
|---|---|
0 | Success |
1 | General error |
2 | Misuse of shell builtins (wrong arguments) |
126 | Command found but not executable |
127 | Command not found |
128+N | Script exited due to signal N (e.g. 130 = Ctrl+C = SIGINT) |
| Any non-zero | Some kind of failure |
You can use any code 1–125 in your own scripts. Keep 0 for success and define your error codes consistently.
Checking and Using Exit Codes
# $? holds the exit code of the last command
ls /nonexistent
echo "Exit code: $?" # 2 (ls error)
grep "pattern" file.txt
echo "Exit code: $?" # 0=found, 1=not found, 2=error
# Use it in conditions
if grep -q "ERROR" logfile.txt; then
echo "Errors found"
fi
# Check multiple commands
mkdir /tmp/work && cd /tmp/work && echo "In /tmp/work"
# ^^^ only runs next command if previous succeeded
# Try something, fall back on failure
config_dir="${XDG_CONFIG_HOME:-$HOME/.config}"
mkdir -p "$config_dir" || { echo "Cannot create config dir" >&2; exit 1; }Setting Your Script's Exit Code
#!/usr/bin/env bash
# exit N — terminates the script with code N
exit 0 # success
exit 1 # general error
# Without an explicit exit, the script exits with the code of the LAST command
echo "hello"
# implicit: exit 0 (echo succeeds)
grep "nonexistent" /etc/hosts
# implicit: exit 1 (grep found nothing)
# Best practice: always be explicit about exit codes
if ! some_command; then
echo "some_command failed" >&2
exit 1
fi
exit 0Custom Exit Code Scheme
#!/usr/bin/env bash
# Define exit codes as constants at the top
readonly E_OK=0
readonly E_USAGE=1
readonly E_MISSING_FILE=2
readonly E_PERMISSION=3
readonly E_NETWORK=4
readonly E_TIMEOUT=5
function backup_file {
local src="$1"
local dst="$2"
[[ $# -ne 2 ]] && return $E_USAGE
[[ ! -f "$src" ]] && return $E_MISSING_FILE
[[ ! -r "$src" ]] && return $E_PERMISSION
cp "$src" "$dst" || return $E_PERMISSION
return $E_OK
}
backup_file "/etc/hosts" "/tmp/hosts.bak"
case $? in
$E_OK) echo "Backup done" ;;
$E_MISSING_FILE) echo "Source file not found" >&2; exit $E_MISSING_FILE ;;
$E_PERMISSION) echo "Permission denied" >&2; exit $E_PERMISSION ;;
*) echo "Unknown error" >&2; exit 1 ;;
esacQuick Check
A script ends without an explicit `exit`. What exit code does it return?
Exercise
Write a function require_commands that:
- Takes one or more command names as arguments
- For each, checks if it's available with
command -v - If ANY command is missing, prints "ERROR: required command not found: COMMAND" (to stderr) and returns 1
- Returns 0 only if ALL commands are present
Then call it: require_commands curl jq git and exit the script if it fails.