| |

LFCS 127 ๐Ÿง Shell Scripting โ€” Error Handling

โฑ๏ธ Estimated read: 12 min | |

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

OptionEffect
set -eExit on error
set -uExit on unset variable
set -o pipefailPipeline fails if any command fails
set -xPrint commands before executing

Strict Mode

CombinationPurpose
set -euo pipefailFull strict mode
set -eBasic error exit

trap Signals

SignalPurpose
EXITScript exit (any)
ERRCommand failure
INTCtrl+C
TERMTermination
HUPHangup

Exit Code Checks

PatternMeaning
if ! cmd; thenRun on failure
cmd || fallbackAlternative on failure
[ $? -eq 0 ]Explicit check
case $? inMultiple 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

PitfallWhy It HappensFix
Script continues after failureNo set -eAdd set -e
Script continues in pipelineNo pipefailset -o pipefail
Trap not runningSignal misspelledUse correct signal name
Cleanup not running on errorTrap only handles EXITUse EXIT trap
Exit code lostIntervening commandCapture 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

ItemValue
Exit on errorset -e
Exit on unset variableset -u
Pipeline failureset -o pipefail
Strict modeset -euo pipefail
Cleanuptrap '...' EXIT
Error handlertrap '...' ERR
Signal handlertrap '...' INT
Explicit checkif ! cmd; then

Key takeaways:

  • set -e is 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 pipefail is the pipeline equivalent. Without it, the pipeline exit code is the last command’s. With it, the pipeline fails if any command fails.
  • trap EXIT is the cleanup mechanism. It runs when the script exits, regardless of the reason. It removes temporary files, kills background processes, and releases resources.
  • trap ERR is the diagnostic mechanism. It runs when a command fails. The $LINENO variable is the line number of the failure.
  • The explicit if check is the most precise. It allows a specific action for a specific failure, rather than a generic exit. The if ! cmd; then pattern 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.

Was this helpful?


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!