LFCS 127 ๐ง Shell Scripting โ Error Handling
The previous chapters covered the syntax of scripts: variables, conditionals, loops, functions, and arguments. This chapter covers the behavior of a script when something goes wrong. A script without error handling fails silently. A command fails, the script continues, and the failure is masked by a later successful command. The exit code is 0, the caller believes the script succeeded, and the actual problem is invisible. The LFCS exam tests the mechanisms that prevent this: set -e, trap, and the explicit if check on exit codes. The critical trap is the set -e behavior in specific contexts: it is ignored in a command that is part of a condition (if cmd, while cmd, cmd || fallback), and it does not catch a failure in a pipeline unless set -o pipefail is also set.
Key point: set -e makes the script exit when any command returns a non-zero exit code. But the command is not checked if it is in a condition (if, while, until), or in a logical && or || chain, or negated with !. The set -o pipefail makes a pipeline fail if any command in it fails, not just the last one. The trap command registers a handler that runs on exit, on error, or on a specific signal. The trap 'cleanup' EXIT is the standard pattern for cleanup.
Why error handling requires explicit mechanisms
The silent failure problem. A shell script runs commands sequentially. If a command fails, the shell continues to the next command unless the script explicitly checks the exit code. The failure is visible in the output if the command prints an error, but the exit code of the script is the exit code of the last command, which may have succeeded. The caller sees a successful exit and assumes the script worked.
The partial state problem. A script that creates a directory, copies files into it, and then fails has left the system in a partial state. The directory exists, the files are incomplete, and the next run may skip the directory because it already exists. The trap command with EXIT is the mechanism for cleaning up: remove the partial directory, close the file descriptor, kill the background process.
The set -e exception problem. The set -e option is not a universal catch-all. It is ignored in a command that is part of a condition: if failing_command; then ... fi does not exit. It is ignored in a logical chain: failing_command || true does not exit. The reason is that these contexts are designed to handle the failure. The exam tests the exception because a script that relies on set -e without understanding the exception will still fail silently.
The pipeline problem. A pipeline runs multiple commands, connected by pipes. The exit code of the pipeline is the exit code of the last command. If the first command fails but the last command succeeds, the pipeline returns 0. The set -o pipefail option changes this: the pipeline fails if any command fails. The set -euo pipefail combination is the standard strict mode.
The trade-off. set -e is the simplest mechanism, but it is also the bluntest. It exits on the first failure, which may not be the correct behavior if the script has an explicit error recovery path. The explicit if check is more verbose but more precise. The trap is the cleanup mechanism, not the error handling mechanism. The exam tests all three.
a. set -e and its exceptions
The set -e option makes the script exit when a command returns a non-zero exit code. The exception is a command in a condition or a logical chain.
#!/bin/bash
set -e
mkdir /tmp/build
cd /tmp/build
touch file.txt
If mkdir fails (because the directory already exists or because of a permissions issue), the script exits before cd. The caller sees the non-zero exit code and can act on it.
The exception is a command that is part of a condition. The following script does not exit if ls /nonexistent fails.
#!/bin/bash
set -e
if ls /nonexistent; then
echo "Exists"
else
echo "Does not exist"
fi
The if ls /nonexistent; then is a condition. The set -e does not apply because the script is explicitly handling the failure.
The same exception applies to a logical chain.
#!/bin/bash
set -e
failing_command || echo "Handled"
The || chain handles the failure. The script continues.
The set -o pipefail option is the pipeline equivalent. Without it, the pipeline exit code is the last command’s.
#!/bin/bash
set -e
# Without pipefail: the pipeline succeeds if the last command succeeds
false | echo "Hello"
echo "This runs"
# With pipefail: the pipeline fails because the first command failed
set -o pipefail
false | echo "Hello"
echo "This does not run"
The set -euo pipefail combination is the standard strict mode. The -u makes the script exit if an unset variable is referenced. The -o pipefail makes the pipeline fail if any command fails.
b. trap: cleanup on exit and signal handling
The trap command registers a handler for a signal or for the EXIT pseudo-signal. The EXIT trap runs when the script exits, regardless of whether the exit is normal or due to an error.
#!/bin/bash
tmpfile=$(mktemp)
trap 'rm -f "$tmpfile"' EXIT
echo "data" > "$tmpfile"
# The file is removed when the script exits.
The EXIT trap is the standard cleanup mechanism. It removes temporary files, kills background processes, and releases resources. The trap runs even if the script is interrupted by Ctrl+C or killed by SIGTERM.
The trap can also handle specific signals. The SIGINT is the Ctrl+C signal. The SIGTERM is the termination signal. The SIGKILL cannot be trapped.
#!/bin/bash
trap 'echo "Interrupted"; exit 1' INT TERM
while true; do
sleep 1
done
The trap runs the handler when the signal is received. The exit 1 terminates the script with a failure code. Without the exit, the script would continue after the handler.
The trap can be removed with trap - SIGNAL. The trap -p command prints the current trap for a signal.
trap - INT
trap -p
The ERR pseudo-signal is a trap that runs when a command fails, similar to set -e. It can be used to log the failure and the line number.
#!/bin/bash
trap 'echo "Error on line $LINENO" >&2' ERR
set -e
failing_command
The $LINENO variable is the line number of the failing command. The ERR trap runs before the script exits. It is the diagnostic mechanism for understanding where the failure occurred.
c. Explicit exit code checks and the if pattern
The most precise error handling is the explicit if check. It tests the exit code of a command and handles the failure with a specific action.
#!/bin/bash
if ! mkdir /tmp/build; then
echo "Failed to create directory" >&2
exit 1
fi
if ! cp file.txt /tmp/build/; then
echo "Failed to copy file" >&2
exit 1
fi
The if ! command; then pattern runs the command and checks the exit code. The ! negates the result, so the then block runs when the command fails. The error message goes to stderr with >&2. The exit 1 terminates the script with a failure code.
The $? variable is the exit code of the most recent command. It can be checked explicitly.
#!/bin/bash
cp file.txt /backup/
if [ $? -ne 0 ]; then
echo "Backup failed" >&2
exit 1
fi
The explicit $? check is more verbose than set -e but more precise. It allows a specific action for a specific failure, rather than a generic exit.
The case statement can be used to handle different exit codes.
#!/bin/bash
ping -c 1 example.com
case $? in
0) echo "Host reachable" ;;
1) echo "Host unreachable" ;;
2) echo "Unknown error" ;;
esac
The ping command returns 0 for success, 1 for unreachable, 2 for other errors. The case statement handles each case with a specific action.
Complete Example Session
# ============================================
# PART 1: SET -E
# ============================================
cat > strict.sh <<'EOF'
#!/bin/bash
set -e
echo "Start"
false
echo "This does not run"
EOF
chmod +x strict.sh
./strict.sh
# Output: Start
echo $?
# Output: 1
# ============================================
# PART 2: SET -E EXCEPTION
# ============================================
cat > condition.sh <<'EOF'
#!/bin/bash
set -e
if false; then
echo "True"
else
echo "False"
fi
echo "Continues"
EOF
chmod +x condition.sh
./condition.sh
# Output: False
# Continues
# ============================================
# PART 3: SET -U
# ============================================
cat > unset.sh <<'EOF'
#!/bin/bash
set -u
echo "$undefined"
EOF
chmod +x unset.sh
./unset.sh
# Output: ./unset.sh: line 3: undefined: unbound variable
# ============================================
# PART 4: SET -O PIPEFAIL
# ============================================
cat > pipefail.sh <<'EOF'
#!/bin/bash
set -e
set -o pipefail
false | echo "Hello"
echo "Does not run"
EOF
chmod +x pipefail.sh
./pipefail.sh
# Output: Hello
# The pipeline fails because the first command failed.
# ============================================
# PART 5: TRAP EXIT
# ============================================
cat > trap.sh <<'EOF'
#!/bin/bash
tmpfile=$(mktemp)
trap 'rm -f "$tmpfile"; echo "Cleaned up"' EXIT
echo "data" > "$tmpfile"
echo "Working"
EOF
chmod +x trap.sh
./trap.sh
# Output:
# Working
# Cleaned up
# ============================================
# PART 6: TRAP SIGNAL
# ============================================
cat > signal.sh <<'EOF'
#!/bin/bash
trap 'echo "Caught SIGINT"; exit 1' INT
while true; do sleep 1; done
EOF
chmod +x signal.sh
./signal.sh
# Press Ctrl+C
# Output: Caught SIGINT
# ============================================
# PART 7: TRAP ERR
# ============================================
cat > errtrap.sh <<'EOF'
#!/bin/bash
trap 'echo "Error on line $LINENO" >&2' ERR
set -e
false
EOF
chmod +x errtrap.sh
./errtrap.sh
# Output: Error on line 4
# ============================================
# PART 8: EXPLICIT CHECK
# ============================================
cat > check.sh <<'EOF'
#!/bin/bash
if ! cp /nonexistent /tmp/; then
echo "Copy failed" >&2
exit 1
fi
EOF
chmod +x check.sh
./check.sh
# Output: cp: cannot stat '/nonexistent': No such file or directory
# Copy failed
# ============================================
# PART 9: CASE ON EXIT CODE
# ============================================
cat > case-exit.sh <<'EOF'
#!/bin/bash
ping -c 1 -W 1 192.0.2.1 > /dev/null 2>&1
case $? in
0) echo "Reachable" ;;
1) echo "Unreachable" ;;
2) echo "Error" ;;
esac
EOF
chmod +x case-exit.sh
./case-exit.sh
# Output: Unreachable
The nine parts trace the error handling: set -e, exception, set -u, set -o pipefail, trap exit, trap signal, trap err, explicit check, and case on exit code.
Quick Reference
set Options
| Option | Effect |
|---|---|
set -e | Exit on error |
set -u | Exit on unset variable |
set -o pipefail | Pipeline fails if any command fails |
set -x | Print commands before executing |
Strict Mode
| Combination | Purpose |
|---|---|
set -euo pipefail | Full strict mode |
set -e | Basic error exit |
trap Signals
| Signal | Purpose |
|---|---|
EXIT | Script exit (any) |
ERR | Command failure |
INT | Ctrl+C |
TERM | Termination |
HUP | Hangup |
Exit Code Checks
| Pattern | Meaning |
|---|---|
if ! cmd; then | Run on failure |
cmd || fallback | Alternative on failure |
[ $? -eq 0 ] | Explicit check |
case $? in | Multiple codes |
Best Practices
โ Do This:
set -euo pipefail
<!-- Use strict mode at the top of the script --> # โ
trap 'rm -f "$tmpfile"' EXIT
<!-- Clean up temporary files on exit --> # โ
if ! cp file /backup/; then exit 1; fi
<!-- Explicit check for critical operations --> # โ
โ Don’t Do This:
# Rely on set -e for all cases
<!-- It is ignored in conditions and logical chains --> # โ
# Use trap without exit in the handler
<!-- The script continues after the handler --> # โ
# Ignore exit codes
<!-- Failures go unnoticed --> # โ
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Script continues after failure | No set -e | Add set -e |
| Script continues in pipeline | No pipefail | set -o pipefail |
| Trap not running | Signal misspelled | Use correct signal name |
| Cleanup not running on error | Trap only handles EXIT | Use EXIT trap |
| Exit code lost | Intervening command | Capture immediately |
Real-World Examples
1. Strict Mode
set -euo pipefail
2. Exit Trap
trap 'rm -f "$tmpfile"' EXIT
3. Error Trap
trap 'echo "Error on line $LINENO" >&2' ERR
4. Signal Trap
trap 'echo "Interrupted"; exit 1' INT
5. Explicit Check
if ! cmd; then exit 1; fi
6. Exit Code Check
cmd
[ $? -ne 0 ] && exit 1
7. Case on Exit Code
case $? in 0) ok ;; 1) fail ;; esac
8. Pipefail
set -o pipefail
9. Unset Variable
set -u
10. Trap Removal
trap - INT
Visual
Error Handling Flow
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ set -e โ
โโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Command returns non-zero? โ
โ โโ YES โ Exit script (unless in condition) โ
โ โโ NO โ Continue โ
โโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ EXIT trap runs โ
โ (cleanup) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
set -e Exceptions
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ if cmd; then ... fi โ
โ โ No exit on failure (condition) โ
โโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ cmd || fallback โ
โ โ No exit on failure (logical chain) โ
โโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ! cmd โ
โ โ No exit on failure (negation) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Summary
| Item | Value |
|---|---|
| Exit on error | set -e |
| Exit on unset variable | set -u |
| Pipeline failure | set -o pipefail |
| Strict mode | set -euo pipefail |
| Cleanup | trap '...' EXIT |
| Error handler | trap '...' ERR |
| Signal handler | trap '...' INT |
| Explicit check | if ! cmd; then |
Key takeaways:
set -eis the basic error exit. It makes the script exit when any command returns a non-zero exit code. The exception is a command in a condition, a logical chain, or a negation.set -o pipefailis the pipeline equivalent. Without it, the pipeline exit code is the last command’s. With it, the pipeline fails if any command fails.trap EXITis the cleanup mechanism. It runs when the script exits, regardless of the reason. It removes temporary files, kills background processes, and releases resources.trap ERRis the diagnostic mechanism. It runs when a command fails. The$LINENOvariable is the line number of the failure.- The explicit
ifcheck is the most precise. It allows a specific action for a specific failure, rather than a generic exit. Theif ! cmd; thenpattern is the standard form.
Remember: Error handling is not a single command. It is a combination: set -euo pipefail for the default behavior, trap EXIT for cleanup, trap ERR for diagnostics, and explicit if checks for the operations that require specific handling. The exam tests the syntax and the exceptions. The script that handles errors is the script that can be trusted in production.
Stop using slow, ad-bloated tool sites! ๐คฎ
๐ Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
โ
Finance (Mortgage, Interest, Inflation)
โ
Tech (Base64, JSON, Dev Suite, IP)
โ
Health (BMI, BMR, TDEE)
โ
Productivity (Timer, Workspace, QR)
โก๏ธ Fast & Private
๐ No data leaves your device
๐ 100% Free
๐ Use it now: https://tools.kandz.me
๐ Bookmark itโyouโll need it later!