Przejdź do treści

⚠️ 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

1
2
3
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:

1
2
3
4
5
6
#!/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:

1
2
set -u
echo "$UNDEFINED_VAR"   # Exits with error

Provide safe defaults:

1
2
3
set -u
: "${VAR:=default}"
echo "$VAR"   # Safe

set -o pipefail

Pipeline exit code reflects first failing command:

1
2
set -o pipefail
false | true   # Exit code: 1 (correct!)
1
set -euo pipefail

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

Detailed Error Information

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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

1
2
# Last command determines exit status
false | true   # Exit status: 0 (misleading!)

With pipefail

1
2
set -o pipefail
false | true   # Exit status: 1 (correct!)

Error Handling in Pipeline Stages

1
2
3
4
5
6
# Handle errors within pipeline
{
    command1 || { echo "command1 failed" >&2; exit 1; }
} | {
    command2 || { echo "command2 failed" >&2; exit 1; }
} | command3

Timeout Protection

1
2
3
4
5
# 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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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

1
2
3
4
5
6
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

1
2
3
4
5
6
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

1
2
3
4
5
6
7
8
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