11.1. Exit Codes#
11.1.1. Common Pitfalls#
1. Forgetting that $? captures the last command
# Bad: $? now contains result of echo, not ls
ls /nonexistent
echo $? # Wrong! This is exit code of echo (0), not ls
# Good: Save immediately
ls /nonexistent
code=$?
echo $code # Correct!
2. Not checking exit codes in critical sections
# Bad: Continues even if mkdir fails
mkdir /mnt/backup
cd /mnt/backup
rm -rf * # DISASTER if cd failed!
# Good: Check and exit
mkdir /mnt/backup || exit 1
cd /mnt/backup || exit 1
rm -rf * # Safe now
3. Losing pipeline errors
# Bad: Only checks wc
cat huge_file | process | wc -l
# If process crashed, you won't notice
# Good: Use pipefail
set -o pipefail
cat huge_file | process | wc -l # Fails if any command fails
4. Ignoring PIPESTATUS[0]
# Mistake: PIPESTATUS[0] is empty after accessing it
cmd1 | cmd2
echo ${PIPESTATUS[0]} # Works
echo ${PIPESTATUS[0]} # Now it's empty (array reset)!
# Save it first
pipeline_codes=${PIPESTATUS[@]}
11.1.2. Real-World Example: Robust Backup Function#
#!/bin/bash
# Production-grade backup with error handling
backup_with_verification() {
local source=$1
local dest=$2
local log_file="/tmp/backup.log"
# Validate inputs
[[ -z "$source" ]] && { echo "Source required" >&2; return 2; }
[[ -z "$dest" ]] && { echo "Dest required" >&2; return 2; }
[[ ! -d "$source" ]] && { echo "Source not found" >&2; return 1; }
# Create backup
echo "[$(date)] Starting backup..." >> "$log_file"
tar -czf "$dest/backup.tar.gz" "$source" 2>>"$log_file" || {
echo "Backup failed" >&2
return 1
}
# Verify backup integrity
tar -tzf "$dest/backup.tar.gz" > /dev/null 2>&1 || {
echo "Backup verification failed" >&2
rm -f "$dest/backup.tar.gz"
return 1
}
echo "[$(date)] Backup successful" >> "$log_file"
return 0
}
# Usage with error handling
if backup_with_verification "/home/user" "/backups"; then
echo "Backup completed successfully"
else
exit_code=$?
echo "Backup failed with code $exit_code"
exit $exit_code
fi
11.1.3. Error Handling Patterns#
11.1.3.1. Pattern 1: Early Return#
safe_operation() {
local file=$1
# Check preconditions immediately
[[ -f "$file" ]] || return 2
[[ -r "$file" ]] || return 13 # Permission denied
# Process file
process "$file" || return 1
return 0
}
11.1.3.2. Pattern 2: Error Accumulation#
# Track if any step failed
backup_data() {
local errors=0
backup_home || ((errors++))
backup_config || ((errors++))
backup_database || ((errors++))
if [[ $errors -gt 0 ]]; then
echo "Failed: $errors backup(s)" >&2
return 1
fi
return 0
}
11.1.3.3. Pattern 3: Try-Catch Simulation#
try() {
[[ $- = *e* ]]; local saved_e=$?
set +e
}
catch() {
export exception_code=$?
(( saved_e )) && set +e
return $exception_code
}
# Usage
try && {
risky_command1
risky_command2
} catch || {
echo "Error occurred with code $exception_code"
}
11.1.4. Handling Errors in Pipelines#
By default, only the last command’s exit code is checked:
# Dangerous: gunzip fails, but script continues
gunzip backup.tar.gz | tar -x | verify_archive
echo $? # Only checks verify_archive, not gunzip!
# Fix: Use pipefail
set -o pipefail
gunzip backup.tar.gz | tar -x | verify_archive
echo $? # Now returns error from any command in pipeline
# Or check individual stages
gunzip backup.tar.gz || { echo "Gunzip failed"; exit 1; }
tar -xf backup.tar || { echo "Tar failed"; exit 1; }
verify_archive || { echo "Verify failed"; exit 1; }
11.1.4.1. PIPESTATUS Array#
# Capture all exit codes from pipeline
cmd1 | cmd2 | cmd3
echo ${PIPESTATUS[@]} # Prints array of all exit codes
# Example
ls /nonexistent 2>/dev/null | grep pattern | wc -l
echo ${PIPESTATUS[@]} # Might print: 2 1 0
# 2 = ls failed
# 1 = grep found nothing
# 0 = wc succeeded
11.1.5. Propagating Errors from Functions#
By default, only the last command’s exit code is returned:
# Bad: This always returns 0!
check_file() {
[[ -f /nonexistent ]]
echo "Checking file"
# Function returns exit code of echo (0), not the test!
}
check_file
echo $? # Prints 0 (wrong!)
# Good: Explicitly return the error code
check_file() {
[[ -f /nonexistent ]] || return 1
echo "File exists"
return 0
}
check_file
echo $? # Prints 1 (correct!)
# Better: Let error propagate naturally
check_file() {
local file=$1
[[ -f "$file" ]] || { echo "File not found: $file" >&2; return 1; }
echo "Processing $file"
}
11.1.6. Understanding Exit Codes#
Every command returns an exit code (status code) indicating success or failure:
0: Command succeeded
1-255: Command failed (reason varies by program)
128+N: Signal termination (128 + signal number)
11.1.6.1. Checking Exit Codes#
# Immediate check with $?
ls /nonexistent
echo $? # Prints 2 (file not found error)
# In conditional
if ls /nonexistent; then
echo "Success"
else
echo "Failed with code $?"
fi
# Direct comparison
grep pattern /nonexistent 2>/dev/null
if [[ $? -eq 0 ]]; then
echo "Found"
elif [[ $? -eq 1 ]]; then
echo "Not found"
elif [[ $? -eq 2 ]]; then
echo "Grep error"
fi
11.1.6.2. Common Exit Codes#
Code |
Meaning |
|---|---|
0 |
Success |
1 |
General error |
2 |
Misuse of command |
127 |
Command not found |
128 |
Invalid signal |
130 |
Terminated by Ctrl+C (SIGINT) |