Bash Exit Codes

Understanding command success and failure indicators

๐Ÿ”ข What are Exit Codes?

Exit codes are numbers returned by commands and scripts to indicate success or failure. A zero means success, while non-zero values indicate different types of errors.

# Check the exit code of the last command
ls /home
echo $?  # Outputs: 0 (success)

ls /nonexistent
echo $?  # Outputs: 2 (error)

Key Exit Code Concepts

โœ…

Success (0)

Command executed successfully

echo "Hello"
echo $?  # 0
โŒ

General Error (1)

Command failed with general error

cat badfile.txt
echo $?  # 1
๐Ÿ”

Check Status ($?)

Variable holding last exit code

command
status=$?
echo $status
๐Ÿšช

Set Exit Code

Return custom exit codes

exit 0  # Success
exit 1  # Failure

๐Ÿ”น Understanding Exit Code 0

Exit code 0 is the universal signal for successful command or script execution in Unix-like systems. When a program finishes without errors, it returns 0 to the shell, indicating that everything proceeded as expected. This convention is used by almost all command-line tools and is critical for scripting logic, where success is often a prerequisite for subsequent steps. Checking for a zero exit status is a fundamental practice in automation, error handling, and conditional execution within pipelines and complex workflows.

#!/bin/bash
# Successful commands return 0

echo "Hello World"
echo "Exit code: $?"

mkdir /tmp/testdir
echo "Exit code: $?"

true  # Always returns 0
echo "Exit code: $?"

Output:

Hello World
Exit code: 0
Exit code: 0
Exit code: 0

๐Ÿ”น Common Exit Codes

Standard exit codes signify specific types of failures, aiding in script debugging and error handling. Code 1 indicates a general error, 2 is for misuse of shell builtins, and 127 means 'command not found'. Codes 126 denote permission issues. Signal-related exits use codes 128+N. Understanding these conventions helps you diagnose why a script failedโ€”was it a typo, a missing file, or a permission problem? Leveraging standard codes in your own scripts makes them more predictable and easier to integrate with other tools.

Standard Exit Codes:

  • 0 - Success
  • 1 - General errors (catchall)
  • 2 - Misuse of shell command
  • 126 - Command cannot execute
  • 127 - Command not found
  • 128 - Invalid exit argument
  • 130 - Script terminated by Ctrl+C
  • 255 - Exit status out of range
#!/bin/bash
# Testing different exit codes

# Command not found (127)
nonexistentcommand
echo "Exit code: $?"

# Permission denied (126)
./no_execute_permission.sh
echo "Exit code: $?"

# General error (1)
cat /nonexistent/file.txt
echo "Exit code: $?"

Output:

bash: nonexistentcommand: command not found
Exit code: 127
bash: ./no_execute_permission.sh: Permission denied
Exit code: 126
cat: /nonexistent/file.txt: No such file or directory
Exit code: 1

๐Ÿ”น Checking Exit Codes with $?

The special variable $? stores the exit status of the most recently executed foreground command. You can immediately check it after any command to determine success (0) or failure (non-zero). For example, ls /nonexistent; echo $? will print a non-zero code. This is the primary mechanism for implementing error-checking logic in scripts. It's crucial to check $? promptly, as its value changes after every command, making it essential for building reliable, fail-safe automation.

#!/bin/bash
# Check exit codes

ls /home
if [ $? -eq 0 ]; then
    echo "Command succeeded"
else
    echo "Command failed"
fi

# Store exit code in variable
grep "pattern" file.txt
exit_status=$?
echo "Grep exit code: $exit_status"

Output:

Command succeeded
Grep exit code: 0

๐Ÿ”น Setting Exit Codes in Scripts

Use the exit command to explicitly define the status your script returns to its caller. For example, exit 0 signals success, while exit 1 indicates failure. You can exit with any integer between 0 and 255. This allows parent processes, cron jobs, or other scripts to understand your script's outcome and act accordingly. Setting meaningful exit codes is a best practice for creating professional, interoperable scripts that integrate seamlessly into larger systems and pipelines.

#!/bin/bash
# script.sh - Example with custom exit codes

if [ $# -eq 0 ]; then
    echo "Error: No arguments provided"
    exit 1  # Exit with error
fi

if [ ! -f "$1" ]; then
    echo "Error: File not found"
    exit 2  # Exit with specific error code
fi

echo "Processing file: $1"
exit 0  # Exit successfully

Usage:

$ ./script.sh
Error: No arguments provided
$ echo $?
1

$ ./script.sh myfile.txt
Processing file: myfile.txt
$ echo $?
0

๐Ÿ”น Exit Codes in Conditionals

Bash conditionals evaluate commands based on their exit codes, where 0 means true and non-zero means false. In if command; then ... fi, the 'then' block runs only if the command succeeds (exits with 0). This is why test commands like [ ] or [[ ]] return 0 for true comparisons. This design elegantly ties command success to logical truth, allowing you to use any command's success/failure as a branching condition, which is fundamental to scripting logic and flow control.

#!/bin/bash
# Using exit codes in if statements

# Direct command in conditional
if grep "error" logfile.txt; then
    echo "Errors found in log"
fi

# Using && (AND) - runs if first succeeds
mkdir /tmp/backup && cp file.txt /tmp/backup/

# Using || (OR) - runs if first fails
cd /nonexistent || echo "Directory not found"

# Combining both
command1 && echo "Success" || echo "Failed"

Output:

Errors found in log
bash: cd: /nonexistent: No such file or directory
Directory not found

๐Ÿ”น Exit Codes with Pipelines

In a command pipeline, the special variable $? only captures the exit code of the last command. To get the status of all commands in the pipeline, Bash provides the ${PIPESTATUS[@]} array. Each element holds the exit code of the corresponding command. For example, after ls | grep file | wc -l, ${PIPESTATUS[1]} holds grep's status. This is essential for accurate error detection in multi-stage data processing where any step's failure matters.

#!/bin/bash
# Exit codes in pipelines

# Only last command's exit code
cat file.txt | grep "pattern" | wc -l
echo "Exit code: $?"

# Check all pipeline commands
cat file.txt | grep "pattern" | wc -l
echo "Pipeline exit codes: ${PIPESTATUS[@]}"
echo "First command: ${PIPESTATUS[0]}"
echo "Second command: ${PIPESTATUS[1]}"
echo "Third command: ${PIPESTATUS[2]}"

Output:

5
Exit code: 0
5
Pipeline exit codes: 0 0 0
First command: 0
Second command: 0
Third command: 0

๐Ÿ”น Error Handling with Exit Codes

Robust error handling using exit codes prevents cascading failures and makes scripts reliable. After critical commands, check $? and use if, &&, ||, or set -e to respond to failures. Provide informative error messages and clean up resources before exiting with a meaningful code. This practice ensures your scripts fail gracefully, log issues appropriately, and don't cause downstream problems, which is crucial for production automation, system tools, and scheduled jobs like cron tasks.

#!/bin/bash
# Robust error handling

# Exit on any error
set -e

# Function with error handling
backup_file() {
    local file=$1
    local backup_dir="/tmp/backup"
    
    if [ ! -f "$file" ]; then
        echo "Error: File not found"
        return 1
    fi
    
    mkdir -p "$backup_dir" || return 2
    cp "$file" "$backup_dir/" || return 3
    
    echo "Backup successful"
    return 0
}

# Use the function
backup_file "important.txt"
if [ $? -eq 0 ]; then
    echo "Backup completed"
else
    echo "Backup failed with code: $?"
fi

Output:

Backup successful
Backup completed

๐Ÿ”น Special Exit Code Behaviors

Bash has unique behaviors related to exit codes that enhance scripting flexibility. The set -e option causes the script to exit immediately if any command fails. The trap command can catch exits and execute cleanup code. Logical operators && and || use exit codes for short-circuit evaluation. Functions return codes via return. Understanding these features allows you to write more concise, fault-tolerant, and maintainable scripts with sophisticated control over program flow and error management.

๐Ÿ”ธ True and False Commands

#!/bin/bash
# Built-in true and false

true
echo "true exit code: $?"  # Always 0

false
echo "false exit code: $?"  # Always 1

# Useful in loops
while true; do
    echo "Running..."
    sleep 1
    break
done

Output:

true exit code: 0
false exit code: 1
Running...

๐Ÿ”ธ Test Command Exit Codes

#!/bin/bash
# Test commands return exit codes

# File exists test
test -f myfile.txt
echo "File test: $?"

# Numeric comparison
[ 5 -gt 3 ]
echo "Comparison: $?"

# String comparison
[ "hello" = "hello" ]
echo "String test: $?"

Output:

File test: 0
Comparison: 0
String test: 0

๐Ÿ”น Exit Codes Best Practices

Adhering to exit code best practices ensures scripts are predictable, maintainable, and professional. Always exit with 0 on success and a non-zero code on failure. Use standard codes where applicable (1 for general errors, 2 for incorrect usage). Document custom exit codes in script headers. Check $? immediately after critical commands. In functions, use return instead of exit. These guidelines improve script debuggability, integration potential, and overall reliability in shared or production environments.

Best Practices:

  • Always return 0 for success - Follow the standard convention
  • Use meaningful error codes - Different errors should have different codes
  • Document exit codes - Comment what each code means
  • Check critical commands - Don't ignore important failures
  • Use set -e cautiously - Understand when scripts should stop
  • Keep codes under 255 - Exit codes wrap around after 255
#!/bin/bash
# Well-structured script with exit codes

# Exit codes
readonly E_SUCCESS=0
readonly E_NO_FILE=1
readonly E_PERMISSION=2
readonly E_NETWORK=3

process_file() {
    local file=$1
    
    # Check if file exists
    if [ ! -f "$file" ]; then
        echo "Error: File not found" >&2
        return $E_NO_FILE
    fi
    
    # Check if readable
    if [ ! -r "$file" ]; then
        echo "Error: No read permission" >&2
        return $E_PERMISSION
    fi
    
    # Process file
    cat "$file"
    return $E_SUCCESS
}

# Main script
process_file "data.txt"
exit_code=$?

case $exit_code in
    $E_SUCCESS)
        echo "Processing completed"
        ;;
    $E_NO_FILE)
        echo "File error occurred"
        ;;
    $E_PERMISSION)
        echo "Permission error occurred"
        ;;
esac

exit $exit_code

๐Ÿ”น Debugging with Exit Codes

Exit codes are invaluable diagnostic tools for identifying where and why a script fails. Strategically insert echo $? statements after commands to trace execution flow. Use set -x to see commands and their exits. Check ${PIPESTATUS[@]} in pipelines. A non-zero code pinpoints the failing command. Combining this with informative error messages allows you to quickly isolate logic errors, external command failures, or environmental issues, streamlining the development and maintenance of complex Bash scripts.

#!/bin/bash
# Debug script with exit code tracking

# Enable exit code debugging
set -x  # Print commands
set -e  # Exit on error

debug_command() {
    local cmd=$1
    echo "Executing: $cmd"
    eval "$cmd"
    local status=$?
    echo "Exit code: $status"
    return $status
}

# Use debug function
debug_command "ls /home"
debug_command "echo 'Hello World'"
debug_command "date"

echo "All commands completed successfully"

Output:

Executing: ls /home
user1  user2
Exit code: 0
Executing: echo 'Hello World'
Hello World
Exit code: 0
Executing: date
Mon Jan 15 10:30:45 UTC 2024
Exit code: 0
All commands completed successfully

๐Ÿ”น Exit Codes in Functions

Functions communicate success or failure to the calling script using the return command with an exit code. The returned value is captured in $? just like a regular command. This allows you to create modular, reusable code blocks that can be tested conditionally. For example, a validation function can return 0 if input is valid and 1 if not, enabling clear, logical flow in the main script. Proper use of function exit codes is key to writing structured, readable, and testable Bash code.

#!/bin/bash
# Functions with exit codes

validate_input() {
    local input=$1
    
    if [ -z "$input" ]; then
        echo "Error: Empty input"
        return 1
    fi
    
    if [ ${#input} -lt 3 ]; then
        echo "Error: Input too short"
        return 2
    fi
    
    echo "Input valid"
    return 0
}

# Test the function
validate_input ""
echo "Exit code: $?"

validate_input "ab"
echo "Exit code: $?"

validate_input "hello"
echo "Exit code: $?"

Output:

Error: Empty input
Exit code: 1
Error: Input too short
Exit code: 2
Input valid
Exit code: 0

๐Ÿง  Test Your Knowledge

What exit code indicates successful command execution?

Which variable stores the last command's exit code?

What does exit code 127 typically mean?