⚠️ Advanced Error Semantics
Understanding how shells handle errors, propagate exit codes, and manage failure scenarios is crucial for writing robust scripts.
🧭 Exit Code Fundamentals
Every command returns an exit code (0-255):
- 0 = Success
- 1-255 = Various failure types
Capturing Exit Codes
| ls /nonexistent
exit_code=$?
echo "Command exited with code: $exit_code"
|
Standard Exit Codes
| Code |
Meaning |
Usage |
| 0 |
Success |
Normal completion |
| 1 |
General error |
Catch-all for unspecified errors |
| 2 |
Misuse of shell builtin |
Incorrect usage |
| 126 |
Command found but not executable |
Permission denied |
| 127 |
Command not found |
Typo or missing PATH entry |
| 130 |
Script terminated by Ctrl+C |
SIGINT received |
| 137 |
Killed by SIGKILL |
kill -9 or OOM killer |
| 143 |
Killed by SIGTERM |
Graceful termination |
Full list in sysexits.h or man sysexits.
🧪 Strict Error Handling
set -e (Exit on Error)
Immediately exit when any command fails:
| #!/bin/bash
set -e
echo "Step 1"
false # Script exits here
echo "Step 2" # Never reached
|
Pitfalls of set -e
1
2
3
4
5
6
7
8
9
10
11
12 | set -e
# ❌ This won't exit despite false
if false; then
echo "Won't print"
fi
# ❌ Neither will this
false || true
# ❌ Nor this
result=$(false)
|
Commands in conditionals or with logical operators don't trigger set -e.
🧠 Enhanced Error Handling
set -u (Undefined Variables)
Catch typos in variable names:
| set -u
echo "$UNDEFINED_VAR" # Exits with error
|
Provide safe defaults:
| set -u
: "${VAR:=default}"
echo "$VAR" # Safe
|
set -o pipefail
Pipeline exit code reflects first failing command:
| set -o pipefail
false | true # Exit code: 1 (correct!)
|
Recommended Combination
This is the "strict mode" used in production scripts.
🧪 Custom Error Handling
Trap ERR
Execute code whenever any command fails:
1
2
3
4
5
6
7
8
9
10
11
12
13 | err_handler() {
echo "Error on line $1" >&2
echo "Last command: $BASH_COMMAND" >&2
echo "Stack trace:" >&2
i=0
while caller $i; do
i=$((i+1))
done
}
trap 'err_handler $LINENO' ERR
false # Triggers handler
|
| error_exit() {
local line_no=$1
local error_code=$2
local error_message=$3
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: $error_message" >&2
echo "Line: $line_no, Code: $error_code" >&2
exit $error_code
}
trap 'error_exit $LINENO $? "Unexpected error"' ERR
|
🧠 Error Propagation Patterns
Function Return Values
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 | validate_input() {
local input="$1"
if [ -z "$input" ]; then
echo "Input cannot be empty" >&2
return 1
fi
if [ ${#input} -lt 3 ]; then
echo "Input too short" >&2
return 2
fi
return 0
}
if ! validate_input "$user_input"; then
echo "Validation failed with code $?" >&2
exit 1
fi
|
Custom Error Codes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 | readonly E_SUCCESS=0
readonly E_INVALID_INPUT=1
readonly E_FILE_NOT_FOUND=2
readonly E_PERMISSION_DENIED=3
readonly E_NETWORK_ERROR=4
check_file() {
local file="$1"
[ -f "$file" ] || return $E_FILE_NOT_FOUND
[ -r "$file" ] || return $E_PERMISSION_DENIED
return $E_SUCCESS
}
check_file config.yml || {
case $? in
$E_FILE_NOT_FOUND) echo "Config file missing" >&2 ;;
$E_PERMISSION_DENIED) echo "Cannot read config" >&2 ;;
esac
exit 1
}
|
🧪 Pipeline Error Handling
Default Pipeline Behavior
| # Last command determines exit status
false | true # Exit status: 0 (misleading!)
|
With pipefail
| set -o pipefail
false | true # Exit status: 1 (correct!)
|
Error Handling in Pipeline Stages
| # Handle errors within pipeline
{
command1 || { echo "command1 failed" >&2; exit 1; }
} | {
command2 || { echo "command2 failed" >&2; exit 1; }
} | command3
|
Timeout Protection
| # Prevent hanging pipelines
if ! timeout 30s command1 | command2 | command3; then
echo "Pipeline failed or timed out" >&2
exit 1
fi
|
🧠 Graceful Degradation
Fallback Mechanisms
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 | get_preferred_editor() {
for editor in "$VISUAL" "$EDITOR" vim nano; do
if command -v "$editor" >/dev/null 2>&1; then
echo "$editor"
return 0
fi
done
echo "No suitable editor found" >&2
return 1
}
EDITOR=$(get_preferred_editor) || {
echo "Falling back to basic editor" >&2
EDITOR=cat
}
|
Optional Features
| enable_advanced_features() {
if command -v jq >/dev/null 2>&1; then
USE_JQ=true
echo "JSON processing enabled"
else
USE_JQ=false
echo "JSON processing disabled (jq not found)"
fi
}
enable_advanced_features
|
🧪 Debugging Error Scenarios
Trace Execution
| set -x # Show commands as executed
set -v # Show input lines as read
# Redirect trace output
BASH_XTRACEFD=7 exec 7>debug.log
set -x
|
Error Logging
| log_error() {
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] ERROR: $*" >&2
}
trap 'log_error "Unexpected error on line $LINENO"' ERR
|
Stack Traces
| show_stack_trace() {
local frame=0
while caller $frame; do
frame=$((frame + 1))
done
}
trap 'echo "Stack trace:" >&2; show_stack_trace' ERR
|
🧾 Summary
- Use
set -euo pipefail for strict error handling
- Trap
ERR for centralized error management
- Define custom error codes for clarity
- Handle pipeline errors with
pipefail
- Implement graceful degradation for optional features
- Log errors with timestamps for debugging
- Use stack traces to identify failure points
- Test error scenarios thoroughly
👉 Continue to: Process Control