100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Bash

Exit Codes and Error Handling

Understand how every command reports success or failure through its exit status, and how to build scripts that detect and respond to errors reliably.

Bash Scripting In DepthIntermediate9 min readJul 9, 2026
Analogies

Exit Codes and Error Handling

Every command that runs on Linux terminates with an integer exit status between 0 and 255, stored by the shell in the special variable $?. By convention, 0 means success and any non-zero value indicates some form of failure, though the specific non-zero meaning is defined by each program. This simple contract is the backbone of shell scripting: it's how if, &&, ||, and while decide what to do next, and it's why disciplined error handling — checking $?, using set -e, and choosing meaningful exit codes in your own scripts — separates fragile automation from production-grade tooling.

🏏

Cricket analogy: Just as the third umpire's decision comes back as a specific code — out for LBW, out for caught, or not-out — every Linux command reports an integer exit status where 0 is 'not out' and any nonzero value is a distinct kind of dismissal.

Reading and Using $?

Immediately after any command runs, $? contains its exit status. Because $? is overwritten by the very next command (even a simple assignment doesn't touch it, but running echo or ls would), you must capture it right away if you need it later: rc=$?. In conditionals, you rarely need $? explicitly — if command; then ... fi already tests the command's exit status directly, and command && next / command || fallback chain execution based on success or failure without ever naming $? at all.

🏏

Cricket analogy: A scorer must jot down the runs off a delivery the instant it's bowled, because the very next ball overwrites the previous tally on the board, so disciplined scorers save the figure to their own notebook before play continues.

bash
#!/usr/bin/env bash
set -euo pipefail

curl -fsSL https://example.com/health -o /tmp/health.json
rc=$?
if [[ $rc -ne 0 ]]; then
    echo "Health check failed with exit code $rc" >&2
    exit 1
fi

# Idiomatic equivalent without capturing $? explicitly
if ! curl -fsSL https://example.com/health -o /tmp/health.json; then
    echo "Health check failed" >&2
    exit 1
fi

# Chaining based on success/failure
mkdir -p /var/backups/app && tar czf /var/backups/app/data.tar.gz /srv/app/data || {
    echo "Backup failed" >&2
    exit 2
}

set -e, set -u, set -o pipefail

set -e (errexit) causes the script to exit immediately if any simple command returns non-zero, instead of silently continuing with a failed step. set -u (nounset) treats references to unset variables as errors, catching typos like $FILENAME when you meant $FILE_NAME. set -o pipefail changes the exit status of a pipeline to be the rightmost non-zero exit status among all its stages, instead of just the last command's — without it, grep pattern file | sort reports success even if grep found nothing, because sort's exit code (0) masks grep's. The idiom set -euo pipefail at the top of a script is considered a best practice for defensive Bash scripting, though it has known sharp edges (see the warning below).

🏏

Cricket analogy: Setting set -e is like a captain instructing that any dropped catch immediately ends the innings review rather than playing on as if nothing happened, while pipefail ensures a mis-fielded ball earlier in a relay throw isn't masked by a clean final throw to the keeper.

By POSIX convention, exit code 0 means success, 1 is a generic catch-all error, 2 often signals shell builtin misuse (e.g. bad syntax), 126 means the command was found but not executable, 127 means command not found, and 128+N means the process was terminated by signal N (e.g. 130 = 128+2 = terminated by SIGINT/Ctrl-C, 137 = 128+9 = killed by SIGKILL, often due to the OOM killer). Well-written scripts and tools document their specific non-zero codes so callers can branch on them (grep, for instance, returns 0 if a match was found, 1 if not, and 2 on an actual error).

set -e does NOT catch every failure. It's silently ignored for commands in conditional contexts (if, while, until, &&/||), for the last command in a pipeline when a preceding pipeline stage's real failure is masked, and for any command whose return value is being tested. A classic footgun: result=$(some_failing_command) does not abort under set -e if some_failing_command is inside a larger conditional context, or when its output is captured in certain function contexts — always test critical commands explicitly rather than relying purely on set -e for correctness-critical logic.

Trapping Errors and Choosing Exit Codes

The trap builtin lets a script run a handler when it receives a signal or exits, which is invaluable for cleanup (removing temp files, releasing locks) regardless of how the script terminates. trap 'cleanup' EXIT runs the cleanup function whenever the script exits, whether normally, via exit, or due to an uncaught error under set -e. When writing your own scripts, exit N at any point terminates immediately with status N; choosing distinct, documented codes (e.g. 1 = generic failure, 2 = bad usage, 3 = missing dependency) makes scripts composable in larger automation pipelines where callers branch on the specific code.

🏏

Cricket analogy: The trap builtin is like a groundskeeper who rolls out the covers whenever the match ends, whether by a result, a draw, or rain stopping play, ensuring the pitch is protected no matter how the day concluded.

bash
#!/usr/bin/env bash
set -euo pipefail

TMPDIR=$(mktemp -d)
cleanup() {
    rc=$?
    rm -rf "$TMPDIR"
    exit "$rc"
}
trap cleanup EXIT
trap 'echo "Interrupted!" >&2; exit 130' INT

echo "Working in $TMPDIR"
curl -fsSL https://example.com/data.csv -o "$TMPDIR/data.csv" || {
    echo "Download failed" >&2
    exit 3
}
echo "Done"
  • $? holds the exit status (0-255) of the most recently executed command; capture it immediately with rc=$? if needed later.
  • 0 means success by convention; any non-zero value means failure, with the specific meaning defined by each program.
  • set -e exits on any unhandled command failure, set -u errors on unset variables, and set -o pipefail propagates failures through pipelines.
  • set -e has notable exceptions (conditionals, pipeline non-final commands, &&/||) — don't rely on it exclusively for critical error paths.
  • trap 'handler' EXIT/INT/TERM runs cleanup code reliably regardless of how or why the script terminates.
  • 128+N as an exit code indicates termination by signal N (e.g. 137 = SIGKILL, 130 = SIGINT).

Practice what you learned

Was this page helpful?

Topics covered

#Bash#LinuxShellScriptingStudyNotes#DevOps#ExitCodesAndErrorHandling#Exit#Codes#Error#Handling#ErrorHandling#StudyNotes#SkillVeris