Linux Basic — Handling Errors Practice Exercises

5 minute read

Handling Errors — Practice Exercises

Five exercises spanning exit codes, strict mode, traps, and helper functions.


Exercise 1 — Build a die That Accepts an Exit Code

Goal: Write a die function that accepts an optional numeric first arg as the exit code (defaulting to 1) and prints all remaining args as the message to stderr. Then exits.

Demonstrate with:

die "generic failure"
die 2 "bad arguments"
die 42 "magic answer"
💡 Show Answer
die() {
    local rc=1
    if [[ "${1:-}" =~ ^[0-9]+$ ]]; then
        rc=$1
        shift
    fi
    printf 'ERROR: %s\n' "$*" >&2
    exit "$rc"
}

# Demo (each in a subshell so script doesn't actually exit)
(die "generic failure")
echo "rc=$?"
# ERROR: generic failure
# rc=1

(die 2 "bad arguments")
echo "rc=$?"
# ERROR: bad arguments
# rc=2

(die 42 "magic answer")
echo "rc=$?"
# ERROR: magic answer
# rc=42

Key points:

  • ${1:-} so set -u doesn’t bite when called with no args
  • =~ ^[0-9]+$ correctly distinguishes a numeric first arg from a message starting with a number
  • printf '%s\n' instead of echo is safer for arbitrary content

Exercise 2 — Self-Cleaning Temp Directory

Goal: Write a script that creates a temp directory, writes a few files in it, then exits. The temp directory must be deleted on every exit path: normal exit, die exit, Ctrl-C, and an unhandled set -e error. Prove it by running the script and checking that $tmpdir no longer exists afterward.

💡 Show Answer
#!/usr/bin/env bash
set -euo pipefail

tmpdir=$(mktemp -d)
echo "Created: $tmpdir"

cleanup() {
    local rc=$?
    rm -rf -- "$tmpdir"
    echo "Cleaned up (exit $rc)"
}
trap cleanup EXIT
trap 'echo "Interrupted"; exit 130' INT

# Create some files
touch "$tmpdir/a.txt" "$tmpdir/b.txt"
ls "$tmpdir"

# Uncomment ONE to test different exit paths:
# exit 0          # normal exit
# false           # set -e abort
# kill -INT $$    # SIGINT to self
# echo "$undef"   # set -u abort

echo "Normal end"

Test all four paths:

$ ./script.sh
Created: /tmp/tmp.XXX
a.txt b.txt
Normal end
Cleaned up (exit 0)
$ ls /tmp/tmp.XXX
ls: cannot access ...: No such file or directory     ✓

Key insights:

  • trap cleanup EXIT covers normal exit, die, set -e abort, and signals (because signals trigger EXIT)
  • The explicit trap … INT is optional but lets you customize the SIGINT message and exit code
  • Capture $? as the very first line of the cleanup function

Exercise 3 — Detect Pipeline Failure

Goal: Write a function fetch_count that runs curl -sS --fail "$url" | wc -l and returns the line count. It must:

  • Fail (return non-zero) if curl fails (e.g., 404)
  • Fail if wc fails
  • Print the count on success

Demonstrate with a real URL and a bogus URL.

💡 Show Answer
fetch_count() {
    local url=$1
    local count rc

    # Local pipefail just for this function
    set -o pipefail
    count=$(curl -sS --fail "$url" | wc -l)
    rc=$?

    if (( rc != 0 )); then
        echo "fetch failed for $url (exit $rc)" >&2
        echo "PIPESTATUS = ${PIPESTATUS[*]}" >&2
        return "$rc"
    fi

    echo "$count"
}

# Good URL
fetch_count https://example.com
# 46

# Bad URL
fetch_count https://example.com/nope-404
# fetch failed for https://example.com/nope-404 (exit 22)
# PIPESTATUS = 22 0

Without pipefail, the bad URL case would print 0 (because wc -l succeeds with empty input) and the function would return success — silently wrong.

PIPESTATUS shows exactly which stage failed: 22 0 means curl (stage 0) returned 22 and wc (stage 1) returned 0.


Exercise 4 — require Dependencies Up Front

Goal: Write require_cmd (binary tools) and require_var (env vars) helpers. Use them at the top of a script that needs jq, curl, awk and the env vars API_TOKEN and REGION. The script must list all missing items in one go before exiting — don’t fail on the first one.

💡 Show Answer
#!/usr/bin/env bash
set -euo pipefail

die() { printf 'ERROR: %s\n' "$*" >&2; exit 1; }

require_cmd() {
    local cmd missing=()
    for cmd in "$@"; do
        command -v "$cmd" >/dev/null 2>&1 || missing+=("$cmd")
    done
    if ((${#missing[@]})); then
        die "missing command(s): ${missing[*]}"
    fi
}

require_var() {
    local v missing=()
    for v in "$@"; do
        [[ -n "${!v:-}" ]] || missing+=("$v")
    done
    if ((${#missing[@]})); then
        die "missing env var(s): ${missing[*]}"
    fi
}

require_cmd jq curl awk
require_var API_TOKEN REGION

echo "All deps present, proceeding..."

Test with nothing set:

$ ./script.sh
ERROR: missing env var(s): API_TOKEN REGION
$ echo $?
1

The key idea: collect first, report once. Don’t die on the first miss — users want to know everything they’re missing in one go.

${!v:-} is indirect expansion with default-empty — safe under set -u.


Exercise 5 — Graceful Long-Running Loop

Goal: Write a script that runs an infinite loop printing the time once per second. On SIGINT (Ctrl-C) or SIGTERM, it should:

  • Print “Caught signal — shutting down”
  • Print a summary: “Ran for N seconds”
  • Exit with code 130 (SIGINT) or 143 (SIGTERM) appropriately
💡 Show Answer
#!/usr/bin/env bash
set -euo pipefail

start=$SECONDS
caught=""

shutdown() {
    local sig=$1
    caught=$sig
    local rc
    case "$sig" in
        INT)  rc=130 ;;
        TERM) rc=143 ;;
        *)    rc=1   ;;
    esac
    echo
    echo "Caught $sig — shutting down"
    echo "Ran for $(( SECONDS - start )) seconds"
    exit "$rc"
}

trap 'shutdown INT'  INT
trap 'shutdown TERM' TERM

while true; do
    printf '%(%H:%M:%S)T\n' -1
    sleep 1
done

Test:

$ ./loop.sh
09:00:01
09:00:02
09:00:03
^C
Caught INT — shutting down
Ran for 3 seconds
$ echo $?
130

Notes:

  • $SECONDS is a Bash built-in counter — seconds since shell start
  • printf '%(%H:%M:%S)T\n' -1 is the built-in time formatter (Bash 4.2+) — no date fork
  • 130 = 128 + 2 (SIGINT), 143 = 128 + 15 (SIGTERM) — standard “killed by signal” convention
  • The trap must call exit explicitly — otherwise the loop continues after the handler returns

For a real worker (with cleanup):

trap 'echo "shutting down"; rm -f "$lockfile"; exit 130' INT TERM

while true; do
    work_one_unit || true
    sleep 1
done

What’s Next?

You can now write Bash scripts that fail predictably, clean up after themselves, and tell users exactly what went wrong. The next chapter is the other half of the equation — debugging scripts that have a bug despite all this defensive coding: set -x, bash -x, ShellCheck, debug modes, and the catalog of “classic” shell bugs.

Continue → Debugging Techniques

Updated: