Sagar.BlogArticle
All posts
All posts
Bash

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

CodeMeaning
0Success
1General error
2Misuse of shell builtins (wrong arguments)
126Command found but not executable
127Command not found
128+NScript exited due to signal N (e.g. 130 = Ctrl+C = SIGINT)
Any non-zeroSome 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 0

Custom 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 ;;
esac
Quick Check

A script ends without an explicit `exit`. What exit code does it return?

Exercise

Write a function require_commands that:

  1. Takes one or more command names as arguments
  2. For each, checks if it's available with command -v
  3. If ANY command is missing, prints "ERROR: required command not found: COMMAND" (to stderr) and returns 1
  4. Returns 0 only if ALL commands are present

Then call it: require_commands curl jq git and exit the script if it fails.