Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Bash & the POSIX Shell  Module 9 – Robust scripts: errors, traps and debugging

Error-handling patterns for scripts

Send errors to stderr with die and warn, test status with if, || and the negation operator, fail early, and choose exit codes a caller can read.

  • Intermediate
  • 10 minutes
  • Examples run with Bash 5.3.20, dash 0.5.13.5, shellcheck 0.11.0 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 10 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Send error messages to stderr while keeping stdout clean for data and pipes
  • Test a command's status with if, || and ! instead of saving $? for later
  • Validate inputs and required commands before any side effect runs
  • Choose exit codes a caller can act on, and summarise batches at the end

Before you start

What you’ll learn · Before you start

What you will learn

  • Send error messages to stderr while keeping stdout clean for data and pipes
  • Test a command's status with if, || and ! instead of saving $? for later
  • Validate inputs and required commands before any side effect runs
  • Choose exit codes a caller can act on, and summarise batches at the end

Before you start

On this page

A nightly job ran export.sh | gzip > daily.csv.gz and reported success for a week, yet each morning’s archive was 43 bytes long: export.sh had printed error: database locked on its standard output, where the data goes, and the pipeline’s status was the 0 of gzip. Nothing in the run said that anything had failed. A few error-handling patterns put the message on standard error and give the caller an exit status it can act on.

You will write errors to stderr while keeping stdout for data, branch on a command’s status with if, || and !, validate inputs and required commands before any side effect, choose exit codes a caller can act on, and keep batches going so the end of the run can list every failure.

Errors go to stderr; data stays on stdout

A shell program reads from standard input, writes data to standard output and writes diagnostics (errors, warnings, progress) to standard error. The split is not a convention of taste — it is the one that lets cmd | other-cmd work. The data stays on stdout, which other-cmd reads; the diagnostics still reach the terminal through stderr. POSIX defines standard error as the stream “for diagnostic output” (its stdin page), and the Google Shell Style Guide asks that every error message go there.

The one-line form is printf '…' >&2:

Write an error to standard error with printf and >&2 Shell · error_to_stderr.sh
#!/bin/bash
# The one-line form for writing an error to standard error.
# Read-only snippet: run it from a bigger script.
printf 'error: no such directory: %s\n' "$dir" >&2
exit 3

>&2 sends the output to file descriptor 2, which is standard error unless the caller redirects it. Redirection: stdin, stdout and stderr explains redirection in full; here the one thing to carry is >&2 for every error print.

Printing an error on stdout, so a pipe swallows it

Wrong: echo "error: cannot read $file" followed by exit 1 in a script that is piped into another command. The next command reads the error as data (or ignores it) and the terminal shows nothing. Right: printf 'error: cannot read %s\n' "$file" >&2, and the error line reaches the terminal even when stdout is captured.

Checkpoint

Check: Errors to stderr; data to stdout

  1. Question 1 of 2 A script writes "error: no such file" to standard output and exits 1. The caller runs it as script | jq .. What does jq do?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: jq reads "error: no such file" as its input, fails to parse, and prints a parse error of its own

    Use printf '%s\n' "$msg" >&2 for every error. A caller that wants the error in a file runs the script with 2>errors.log.

  2. Question 2 of 2 Which of these writes the error to standard error?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: printf 'error: no such file\n' >&2

    The short form everyone writes is >&2; it reads as "and send that to fd 2".

Small helpers for consistent errors: die, warn and a tool check

Two one-line helpers make every error look the same, and need_cmd fails early when a required outside tool is missing. errs.sh is the whole library:

die, warn and need_cmd; a usage error, a missing input, a bad record, and a clean run Python · die_warn_session.py
from session import run, write

# A small library of error helpers. die prints to stderr and exits with a status, warn prints to stderr and returns,
# need_cmd checks for an outside command before any side effect runs. The script that uses them handles the three
# error paths: usage error (exit 2), missing input (exit 3), and record failure (exit 4), with messages a caller can
# read and act on.
write("errs.sh", """# Error helpers for the lesson's small library. source this file.
die()  { printf 'error: %s\\n' "$*" >&2; exit "${rc:-1}"; }
warn() { printf 'warning: %s\\n' "$*" >&2; }

need_cmd() {
    for c in "$@"; do
        if ! command -v -- "$c" >/dev/null 2>&1; then
            printf 'error: required command not found: %s\\n' "$c" >&2
            exit 1
        fi
    done
}
""")

write("import.sh", """#!/bin/bash
set -euo pipefail
# shellcheck source=errs.sh
source ./errs.sh

usage() {
    printf 'usage: import.sh <records-dir>\\n' >&2
    rc=2 die "wrong arguments"
}

[ $# -eq 1 ] || usage
dir=$1
[ -d "$dir" ] || { rc=3 die "no such directory: $dir"; }
need_cmd awk sort

failed=0
# Each file holds one record per line in the form "id,amount". A record whose amount is not above 0 counts as failed.
while IFS= read -r -d '' f; do
    while IFS=, read -r id amount; do
        if ! awk -v a="$amount" 'BEGIN { exit !(a > 0) }'; then
            warn "bad amount in $f: id=$id amount=$amount"
            failed=$((failed + 1))
            continue
        fi
        printf 'imported %s %s from %s\\n' "$id" "$amount" "$(basename -- "$f")"
    done < "$f"
done < <(find "$dir" -type f -name '*.csv' -print0 | sort -z)

if (( failed > 0 )); then
    rc=4 die "$failed record(s) failed; see warnings above"
fi
echo "every record imported"
""")

write("make_records.sh", """mkdir -p records
printf 'A,10\\nB,-5\\n' > records/jan.csv
printf 'C,20\\n' > records/feb.csv
""")

run("""
chmod +x import.sh make_records.sh
# usage error (no argument): exit 2 with a usage message to stderr
!bash import.sh
# missing input (bad path): exit 3
!bash import.sh ./no-such-dir
# normal run with a bad amount in one record: exit 4 with a count
bash make_records.sh
!bash import.sh ./records
# recover by writing the record cleanly and run again: exit 0
printf 'A,10\\nB,5\\n' > records/jan.csv
bash import.sh ./records
""")

Output

$ chmod +x import.sh make_records.sh
# usage error (no argument): exit 2 with a usage message to stderr
$ bash import.sh
usage: import.sh <records-dir>
error: wrong arguments
[exit status 2]
# missing input (bad path): exit 3
$ bash import.sh ./no-such-dir
error: no such directory: ./no-such-dir
[exit status 3]
# normal run with a bad amount in one record: exit 4 with a count
$ bash make_records.sh
$ bash import.sh ./records
imported C 20 from feb.csv
imported A 10 from jan.csv
warning: bad amount in ./records/jan.csv: id=B amount=-5
error: 1 record(s) failed; see warnings above
[exit status 4]
# recover by writing the record cleanly and run again: exit 0
$ printf 'A,10\nB,5\n' > records/jan.csv
$ bash import.sh ./records
imported C 20 from feb.csv
imported A 10 from jan.csv
imported B 5 from jan.csv
every record imported

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 die_warn_session.py

die writes error: … to stderr and exits with the status in $rc (default 1), so the caller writes rc=2 die "wrong arguments" on a usage error and rc=3 die "no such directory" on a missing input. warn writes warning: … to stderr and returns without exiting, so the caller can count the warnings and decide later. The session’s run showed four separate paths: the usage error with exit 2, the missing input with exit 3, the run with a bad record ending at exit 4 with the counts, and the clean run after the record was fixed.

need_cmd is the pattern for every outside tool the script relies on: command -v -- "$c" >/dev/null 2>&1 returns 0 when the shell can run the name c, whether it is a builtin, a function or a program on PATH. Checking the tools before the first side effect is the fail-early rule, which the next section unfolds.

Key idea

A one-line die and a one-line warn are enough for every script in this module: consistent prefixes, errors on stderr, and an exit code the caller can act on.

Test a command’s status directly instead of saving it

A command’s exit status: The number from 0 to 255 every command ends with, kept by the shell in $?. 0 means success; Bash uses 126 for found but not executable and 127 for not found. More in “Your first script: shebang, chmod and running it” is a decision, so read it where the decision is made: as the condition of if (or while or until), on the left of || or &&, or after !. Those are also the places where set -e looks away, so a failure read there is handled instead of ending the script. Testing $? a few lines later is the slip ShellCheck’s SC2181 names, because every command in between sets $? to its own status.

Four patterns for reading a status, and the SC2181 slip that reports WARN found when WARN was absent Python · status_session.py
from session import run, write

# Four patterns for reading an exit status directly, and the slip ShellCheck's SC2181 names: saving $? and testing it
# several lines later, when another command has already overwritten it.
write("status.sh", """#!/bin/bash
set -euo pipefail

# 1) Branch with "if cmd": the status is the condition; no "$? -eq 0" needed.
if grep -q error today.log; then
    echo "today has errors"
else
    echo "today is clean"
fi

# 2) Short-circuit with ||: handle the failure in-line and keep errexit on.
cd ./workspace || { echo "cannot enter workspace" >&2; exit 3; }
echo "in workspace: $PWD"
cd - >/dev/null

# 3) Negated condition with !: run a branch when the command fails.
if ! command -v nosuch_tool >/dev/null 2>&1; then
    echo "nosuch_tool is not installed; falling back to the shell builtin"
fi

# 4) Keep the number when it matters. Under set -e a plain "grep ...; rc=$?" would end the script when grep
#    returns 1, so the status is saved on the right of ||, where errexit looks away.
rc=0
grep -q panic today.log || rc=$?
case $rc in
    0)  echo "today has panic lines" ;;
    1)  echo "today has no panic lines (grep returned 1)" ;;
    *)  echo "grep itself failed with status $rc" >&2 ;;
esac
""")

write("sc2181.sh", """#!/bin/bash
# The SC2181 anti-pattern: save $? and test it several lines later. By then another command has reset $?.
# today.log holds "warn:" (lowercase). grep -q WARN returns 1 (no match), so the real answer is "no WARN".
grep -q WARN today.log
echo "about to test the status"
touch marker
if [ $? -ne 0 ]; then
    echo "no WARN found"
else
    echo "WARN found"
fi
""")

write("today.log", "fine\nwarn: cache expired\nerror: upstream 503\nfine\n")

run("""
chmod +x status.sh sc2181.sh
mkdir -p workspace
cat today.log
bash status.sh
echo "---the SC2181 anti-pattern:---"
bash sc2181.sh
!shellcheck sc2181.sh
""")

Output

$ chmod +x status.sh sc2181.sh
$ mkdir -p workspace
$ cat today.log
fine
warn: cache expired
error: upstream 503
fine
$ bash status.sh
today has errors
in workspace: ~/demo/workspace
nosuch_tool is not installed; falling back to the shell builtin
today has no panic lines (grep returned 1)
$ echo "---the SC2181 anti-pattern:---"
---the SC2181 anti-pattern:---
$ bash sc2181.sh
about to test the status
WARN found
$ shellcheck sc2181.sh

In sc2181.sh line 7:
if [ $? -ne 0 ]; then
     ^-- SC2181 (style): Check exit code directly with e.g. 'if ! mycmd;', not indirectly with $?.

For more information:
  https://www.shellcheck.net/wiki/SC2181 -- Check exit code directly with e.g...
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 status_session.py

The four patterns the session shows are the whole kit:

  • if cmd; then …; else …; fi — the condition is the command. No [ $? -eq 0 ]; it would read the same two values as cmd itself.
  • cmd || { handler; exit 3; } — a short-circuit that handles the failure on the spot, with set -e still on. ShellCheck SC2164 reports a bare cd "$dir" and asks for the || exit form, which keeps a cd that failed from running the next line in the wrong folder.
  • if ! cmd; then …; fi — the negation. ! inverts the status and lets a branch run on the failure.
  • rc=0; cmd || rc=$?; case $rc in … — for the case where the number itself matters. Under set -e, a plain cmd; rc=$? never reaches the second command when cmd fails; on the right of || the status is saved instead, and the session’s grep -q panic ended with today has no panic lines (grep returned 1).

The anti-pattern in sc2181.sh was a short run with grep -q WARN today.log (no match, $? was 1), then an echo and a touch ($? reset to 0), then if [ $? -ne 0 ]; then echo "no WARN"; else echo "WARN found"; fi. The test read 0 -ne 0, which is false, so the script printed “WARN found” even though the log did not contain WARN. ShellCheck 0.11.0 reports this as SC2181.

Checkpoint

Check: Test status with if, || and !, not $? saved

  1. Question 1 of 2 A script needs to run a different branch when a file does not contain any lines matching "ERROR". Which form is right?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: if ! grep -q ERROR log; then …; fi

    Read a status where the decision is made: as the condition of if, while or until, on the left of || or &&, or after !. When the number itself matters, save it with rc=0; cmd || rc=$?, which also survives set -e.

  2. Question 2 of 2 sc2181.sh contains grep -q WARN today.log; echo "about to test"; touch marker; if [ $? -ne 0 ]; then echo "no WARN"; else echo "WARN found"; fi. Why did it print "WARN found" even though WARN is not in the log?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: touch marker returned 0 and reset $? to 0; the test [ 0 -ne 0 ] is false, so the else branch ran

    ShellCheck SC2181 names this pattern and prescribes the fix: write the status read as the condition itself, if ! grep -q WARN today.log; then …; fi.

Fail early: validate inputs before any side effect

A failure that stops a script after it has already written half the output is harder to clean up than a failure before any output. The order of the first lines of import.sh is the whole pattern — check the arguments, the input, and the required commands, then start work:

Interactive · step by step Validate before you act, exit with the right code Step through the ways import.sh can end: usage (2), missing input (3), missing tool (1), bad record (4), clean (0).
Validate before you act, exit with the right code, step 5 of 5. Showing import.sh runs, Argument count, Directory exists, Tools present, Each record, Done, exit 0, Usage, exit 2, No input, exit 3, No tool, exit 1, Bad record, exit 4. In focus: Done, exit 0.one argumenta directorytools foundno failuresnone or twonot a folderone missingany failedimport.sh runs0 or 1 argumentArgument countexactly oneDirectory exists[ -d "$dir" ]Tools presentneed_cmd awk sortEach recordwarn, count failuresDone, exit 0all importedUsage, exit 2usage on stderrNo input, exit 3names the pathNo tool, exit 1names the commandBad record, exit 4count in message

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. Usage error, exit 2. Called with no argument, import.sh fails its first check. usage printed "usage: import.sh <records-dir>" and rc=2 die printed "error: wrong arguments", both on stderr, and the session showed status 2.
  2. Missing input, exit 3. With one argument that names no folder, the second check fails: die printed "error: no such directory: ./no-such-dir" and the script ended with status 3, a code of its own for a wrong input.
  3. Missing tool, exit 1. The third check runs need_cmd awk sort before any record is read. Had either tool been missing, it would have named the command on stderr and exited 1. The session had both, so it went on.
  4. Bad records, exit 4. The loop read every record, printed the good ones on stdout and sent one warning to stderr for B, whose amount -5 is not above 0. At the end rc=4 die reported "1 record(s) failed" and the status was 4.
  5. Clean run, exit 0. After B was fixed, the same call printed three imported lines on stdout, nothing on stderr, and "every record imported", with status 0. Every way out is on the right, and only the clean run reaches the bottom.

Three checks come before the first record is read. When the argument count is wrong, usage prints the one-line usage message and dies with 2. When the directory does not exist, die prints the path and dies with 3. When a required command is missing, need_cmd prints its name and exits with 1. In each case no record is processed and stdout stays empty, so the caller’s pipe reads nothing instead of half a result.

The habits the pattern names are small:

  • [ $# -eq 1 ] || usage — the argument count is the first thing. Bash’s own builtins return 2 for incorrect usage (the Bash manual, Exit Status), and import.sh follows them with rc=2 die.
  • [ -d "$dir" ] || die "no such directory: $dir" — the input is the second. -d checks for a directory; -e for any kind of file; -r for a readable file.
  • need_cmd awk sort — every outside tool is checked once, at the top, so a run that misses a tool fails before any record is written.

Fail-early also applies inside the loop: when a record is bad, the loop collects it as a failure instead of writing a half-record. The next section covers that half of the pattern.

Checkpoint

Check: Validate inputs and tools before any side effect

  1. Question 1 of 2 Which of these checks belong at the top of a script, before any side effect?

    Choose every answer that is right.

    Show the answer: Question 1 of 2

    Answer:

    • need_cmd awk sort curl
    • [ -d "$dir" ] || die "no such directory: $dir"
    • [ $# -eq 1 ] || usage

    Reorder a script until nothing side-effectful runs before every check it needs has passed. The import.sh in the session showed the pattern: usage, directory, commands, then records.

  2. Question 2 of 2 A backup script without set -e runs cd "$site_dir" on its own, and the next line is tar -czf /backup/site.tgz .. The folder does not exist. What happens, and which ShellCheck code names the fix?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: cd prints an error and returns non-zero, the script stays where it was, and tar archives that folder instead. ShellCheck SC2164 asks for cd ... || exit.

    Write cd -- "$dir" || exit 3 (or || return inside a function). The || form handles the failure on the spot, with or without set -e.

Choose exit codes on purpose, and summarise batches

Exit codes are the one thing a caller reads without any output. 0 means success and every other value means some kind of failure, which each program defines for itself: POSIX grep exits 0 when a line matched, 1 when none did and more than 1 on an error, and Bash’s builtins return 2 for incorrect usage. A script should choose its codes on a scale like that and document them, so a caller can branch on the number:

Code Meaning When to use
0 Success Everything worked
1 General failure An unexpected error, no better code fits
2 Usage error Wrong arguments, missing option, bad format
3+ Specific failure One per class the caller may want to retry or ignore
126 Could not execute The shell found the file but could not run it
127 Command not found The shell could not find a command on PATH
128 + N Killed by signal N 130 Ctrl-C, 141 SIGPIPE, 143 TERM (earlier lessons)

Statuses above 125 already mean something to a calling shell (the Bash manual: “the shell may use values above 125 specially”): 126 and 127 for a command that could not run, 128 + N for one killed by signal N. Keep a script’s own codes at 125 or below. And an exit code is a reason, not a count: the GNU Coding Standards warn that a status is 8 bits, so a count of 256 errors would arrive as 0 and read as success. import.sh picked 2, 3 and 4: a usage error (rc=2 die), a missing input (rc=3 die) and record failures (rc=4 die). A later wrapper could then retry on 4 (data problem) without retrying on 2 (bug in the call).

For a batch, keeping going is almost always better than stopping at the first failure. Record the id, the file and the reason in an array; at the end, print a summary and die with the right code:

A batch pattern: collect failures in an array, summarise, exit non-zero Shell · batch_summary.sh
#!/bin/bash
# A batch pattern: collect failures as the loop runs, then summarise and exit non-zero.
# Read-only snippet; die, warn and process are expected to come from the script's own helpers.
failed=()
for record in "${records[@]}"; do
    if ! process "$record"; then
        failed+=("$record")
        warn "failed: $record"
    fi
done

if (( ${#failed[@]} > 0 )); then
    printf 'summary: %d/%d records failed\n' "${#failed[@]}" "${#records[@]}" >&2
    printf '  %s\n' "${failed[@]}" >&2
    rc=4 die "see the summary above"
fi

The caller sees exit 4 and the summary on stderr; the stdout stream carries the records that did work. A rerun of the batch only needs the ids in the summary, not every record.

Checkpoint

Check: Exit codes on purpose, and batch summaries

  1. Question 1 of 2 A caller script runs import.sh "$dir" and wants to retry on a bad record (exit 4) but not on a usage error (exit 2). Which test is the right one?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: rc=0; import.sh "$dir" || rc=$?; if [ "$rc" = 4 ]; then retry; elif [ "$rc" != 0 ]; then exit "$rc"; fi

    Document the codes in the script's comment or --help, so a caller can rely on them. Pair them with warn-and-count, so a bad batch exits 4 instead of 0.

  2. Question 2 of 2 A batch processes 1,000 records; 7 fail. What should the script print, and what should its exit status be?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The 993 successes on stdout, each failure as a warn line on stderr, a summary with the counts on stderr at the end, and exit 4 (or another non-zero code the caller can read)

    Keep going is almost always better than stop-first: the caller can rerun the failed ids, instead of re-running the whole batch.

Practise: writing the right error path

The questions start from the two sessions and change one thing: where an error is written, a status read too late, a check that comes after the work, a code a caller must act on. Decide the outcome before you check:

Practice that adapts to you

Practise this lesson

10 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 10 Which number is file descriptor for standard error in a Bash script?

    Choose one answer.

    Show the answer: Question 1 of 10

    Answer: 2

    2>errors.log captures stderr; 2>&1 sends stderr to the same place as stdout; >&2 sends the output of a command to stderr.

  2. Question 2 of 10 A script writes error: cannot read $file with >&2. The caller runs it as script | jq .. Where does the error appear?

    Choose one answer.

    Show the answer: Question 2 of 10

    Answer: On the terminal: stderr is not redirected by the pipe

    The clean split between the two streams is what makes script | jq . work: data on stdout, diagnostics on stderr.

  3. Question 3 of 10 die() { printf 'error: %s\n' "$*" >&2; exit "${rc:-1}"; }. What exit status does rc=3 die "missing input" give?

    Choose one answer.

    Show the answer: Question 3 of 10

    Answer: 3

    Pair die with specific codes (2 for usage, 3 for missing input, 4 and up for the script's own classes).

  4. Question 4 of 10 need_cmd uses command -v -- "$c" >/dev/null 2>&1. Why command -v and not which?

    Choose one answer.

    Show the answer: Question 4 of 10

    Answer: command is a POSIX builtin and reports builtins and functions as well as PATH; which is not standardised and depends on the OS

    POSIX.1-2024 specifies command, -v included, among its utilities, and the Bash manual lists it with the Bash builtins.

  5. Question 5 of 10 ShellCheck wiki says: use cd … || exit. Which rule number is it?

    Choose one answer.

    Show the answer: Question 5 of 10

    Answer: SC2164

    Each ShellCheck code names one slip; the four this module names (SC2155, SC2064, SC2164 and SC2181) cover many of the mistakes in error handling.

  6. Question 6 of 10 Which exit status do Bash builtins return for incorrect usage, such as an invalid option?

    Type a number.

    Show the answer: Question 6 of 10

    Answer: 2

    The Bash manual's Exit Status section says every builtin returns 2 for incorrect usage, generally an invalid option or a missing argument. import.sh followed that with rc=2 die for the "no argument given" path.

  7. Question 7 of 10 A caller receives exit status 139 from the script. What does 139 say?

    Choose one answer.

    Show the answer: Question 7 of 10

    Answer: The script was killed by signal 11 (SIGSEGV): 128 + 11

    139 and its neighbours come up when a child program is killed; in the first lesson of this module, pipefail made the 141 of a writer killed by SIGPIPE the pipeline's own status.

  8. Question 8 of 10 A batch script wants to list the failed ids on stderr at the end. Which Bash expansion prints every id on its own line?

    Choose one answer.

    Show the answer: Question 8 of 10

    Answer: printf ' %s\n' "${failed[@]}" >&2

    printf ' %s\n' "${array[@]}" prints each element indented and on its own line, which reads well in a summary.

  9. Question 9 of 10 What does ShellCheck SC2181 recommend?

    Choose one answer.

    Show the answer: Question 9 of 10

    Answer: Check the exit status directly as the condition: if ! cmd; then … fi, not cmd; if [ $? -ne 0 ]; then … fi

    Read the status as the condition (if cmd, if ! cmd) or on the left of ||; save it with rc=0; cmd || rc=$? only when the number itself matters.

  10. Question 10 of 10 A health check tries curl, and when curl fails (any non-zero) it should write "unknown" to the status file. Which line is the clearest?

    Choose one answer.

    Show the answer: Question 10 of 10

    Answer: curl --fail -sS "$url" > status || printf 'unknown\n' > status

    cmd || handler is the compact form of "when cmd fails, run handler". Keep the handler small: a one-line fallback or an exit 3.

Interview questions

Warm-up (fresher to 2–5 years): why do errors go to stderr instead of stdout, and how is >&2 read?

A program’s standard output is the data that the next command of a pipe, or a file, receives; standard error is a separate stream that stays on the terminal unless the caller redirects it. Writing errors to stderr keeps the pipeline working: a user who runs script | jq sees the data reach jq and the error reach the screen, instead of the error turning into unparseable input for jq. >&2 means “send this command’s output to file descriptor 2”, which is standard error (1 is standard output and 0 standard input; POSIX numbers them this way). So printf 'error: %s\n' "$msg" >&2 writes the formatted line to stderr, and a caller can keep the two streams apart with 2>errors.log.

Two questions follow: an import that reported success with records missing, and a build step that worked in the wrong folder although every status was 0. Note what you would look at first in each before opening it:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A nightly import reads records.csv with while IFS=, read -r id amount && [ -n "$id" ]; do import_one; done. It ran green for weeks. The night the export held a blank line after its third record, the job imported 3 records, skipped the 180 below it, and still exited 0. Explain why, and make such a shortfall impossible to miss.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A deploy job runs cd build && make install from its checkout. On one CI runner it installed another project's files: the job log showed an unexpected absolute path just before make ran, every status was 0, and ShellCheck reports nothing. cd did not fail. What happened, and how do you make the script immune?

    Model answer, Pro. First step and follow-ups

Flashcards · 7 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. Why do error messages go to standard error?

    Standard output is the data the next command or file receives. With errors on standard error, script | jq still gives jq clean data while the error stays on the screen.

  2. What do the die and warn helpers of a script do?

    die writes error: and the message to standard error and exits with the status in rc, 1 by default, so rc=2 die "wrong arguments" marks a usage error. warn writes warning: and the message to standard error and returns without exiting.

  3. Where should a script read a command's status?

    Where the decision is made: if cmd, cmd || ..., ! cmd, or rc=0; cmd || rc=$? when the number matters. Testing $? after another command has run is ShellCheck's SC2181 slip.

  4. Why does cmd; rc=$? never record a failure in a set -e script?

    The failing cmd stops the script before rc=$? runs. Write rc=0; cmd || rc=$? so the failure is handled and its status kept.

  5. What should a script check before its first side effect?

    The argument count, the input and every command it needs, so a broken call stops before it leaves any partial state behind.

  6. Which exit codes should a script choose for its own failures?

    2 for a usage error, as Bash's builtins use it, and 3 and up for failures of its own, kept at 125 or below, since callers read 126, 127 and 128 plus N as could not run or killed by a signal.

  7. How should a script handle failures in a batch of items?

    Keep going, collect the failed items in an array, print a summary at the end and then exit non-zero, so the caller can retry only the failures.

Key takeaways

  • Errors go to stderr (>&2) so stdout stays a clean data stream for pipes and redirects; a one-line die and a one-line warn are enough for every script in this module.
  • Read a command’s status where the decision is made: if cmd, cmd || …, ! cmd, and rc=0; cmd || rc=$? when the number matters. Testing $? after another command has run is ShellCheck’s SC2181 slip; the status is gone by then.
  • Fail early: check the argument count, the input and every required command before the first side effect, so a broken call leaves no partial state.
  • Choose exit codes a caller can act on: 2 for a usage error, as Bash’s builtins do, and 3 and up for failures of your own, kept at 125 or below, because a calling shell reads 126, 127 and 128 + N as “could not run” and “killed by a signal”.
  • For a batch, keep going, collect the failures in an array, summarise them at the end, then die with the right code: the caller can retry only the failures without re-running the whole set.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.