Chapter 15Lesson 01~70 minutes

Strict Mode, Traps, Cleanup, and Temporary Resources

Design Bash automation that fails deliberately, reports useful context, releases temporary resources, and does not mistake a collection of shell options for a complete reliability strategy.

Failure policyTraps and cleanupSecure temporary state

Learning objectives

By the end of this lesson

  • Explain what errexit, nounset, pipefail, and errtrace change—and where they do not apply.
  • Build EXIT, ERR, INT, and TERM traps without losing the original status.
  • Create temporary files and directories securely with mktemp.
  • Separate cleanup from rollback and make both idempotent.
  • Construct a failure-injection lab that proves cleanup behavior.

1. “Strict mode” is an option policy, not a safety theorem

A common Bash preamble is set -Eeuo pipefail. It can expose mistakes earlier, but each option has context-dependent semantics. Reliable scripts treat these options as one layer inside an explicit error contract: commands with expected nonzero statuses are handled intentionally, critical mutations are checked directly, cleanup is registered before risky work, and tests exercise both success and failure paths.

Reliable Bash failure and cleanup lifecycle
flowchart TD
  S["Start automation"] --> O["Apply documented shell options"]
  O --> R["Register cleanup before allocating resources"]
  R --> V["Validate arguments and dependencies"]
  V --> W["Perform one controlled unit of work"]
  W --> C{"Command status expected?"}
  C -- yes --> H["Handle status explicitly"]
  C -- no --> F["Capture context and fail"]
  H --> N{"More work?"}
  N -- yes --> W
  N -- no --> X["EXIT trap performs idempotent cleanup"]
  F --> X
  X --> E["Return original exit status"]
Do not cargo-cult the preamble

set -e has exceptions in tests, loops, command lists, pipelines, subshells, and command substitutions. A refactor can change whether a failure terminates the script. Important failure behavior should remain visible in if, case, or explicit status checks.

2. Understand the options independently

OptionWhat it changesWhat it does not guarantee
-e / errexitMay exit after an unhandled nonzero command statusUniversal fail-fast behavior in every shell context
-u / nounsetErrors on many expansions of unset parametersValidation that a set value is non-empty or semantically valid
pipefailMakes a pipeline fail when a component failsIdentification or recovery from the failing component
-E / errtracePropagates ERR traps into functions, substitutions, and subshellsExecution of ERR in contexts where errexit is suppressed
#!/usr/bin/env bash
set -Eeuo pipefail

# Expected negative results belong inside explicit control flow.
if grep -q '^enabled=true$' service.conf; then
  printf 'feature is enabled\n'
else
  status=$?
  if (( status == 1 )); then
    printf 'feature is not enabled\n'
  else
    printf 'grep failed with status %d\n' "$status" >&2
    exit "$status"
  fi
fi

# nounset does not replace semantic validation.
: "${DEPLOY_ENV:?DEPLOY_ENV is required}"
[[ $DEPLOY_ENV =~ ^(dev|staging|prod)$ ]] || {
  printf 'Unsupported DEPLOY_ENV: %s\n' "$DEPLOY_ENV" >&2
  exit 2
}

Use set -o and shopt -p to capture the effective shell policy during diagnostics. For reusable libraries, avoid silently changing the caller’s global options; either document the requirement or run sensitive code in a controlled subshell.

3. Traps must preserve status and avoid recursive failure

An EXIT trap runs when the shell exits normally or because of an error. Capture $? as the first operation, disable recursive traps when necessary, perform best-effort cleanup, and return the original status. ERR is useful for diagnostics, but it is not a universal exception handler. Signal traps should translate interruption into a clean exit while allowing the EXIT trap to perform cleanup once.

#!/usr/bin/env bash
set -Eeuo pipefail

work_dir=''

cleanup() {
  local status=$?
  trap - EXIT ERR INT TERM

  if [[ -n $work_dir && -d $work_dir ]]; then
    rm -rf -- "$work_dir" || \
      printf 'warning: cleanup failed for %s\n' "$work_dir" >&2
  fi

  exit "$status"
}

on_error() {
  local status=$?
  printf 'error: status=%d source=%s line=%s command=%q\n' \
    "$status" "${BASH_SOURCE[1]:-${BASH_SOURCE[0]}}" \
    "${BASH_LINENO[0]:-unknown}" "$BASH_COMMAND" >&2
  return "$status"
}

trap cleanup EXIT
trap on_error ERR
trap 'exit 130' INT
trap 'exit 143' TERM

work_dir=$(mktemp -d -- "${TMPDIR:-/tmp}/devops-academy.XXXXXXXX")
printf 'workspace=%s\n' "$work_dir"

Keep trap handlers conservative. Calling complex functions, network clients, package managers, or commands that depend on partially initialized state can obscure the original failure. Diagnostics should be concise and cleanup should be repeatable.

4. Temporary resources require secure creation and ownership

Predictable names such as /tmp/report.$$ are unsafe because another process can pre-create or redirect them. mktemp creates a unique file or directory atomically. Prefer a private temporary directory, place all related artifacts inside it, set a restrictive umask, and register cleanup immediately after creation.

old_umask=$(umask)
umask 077

tmp_dir=$(mktemp -d -- "${TMPDIR:-/tmp}/release-check.XXXXXXXX")
trap 'status=$?; rm -rf -- "$tmp_dir"; umask "$old_umask"; exit "$status"' EXIT

manifest="$tmp_dir/manifest.json"
log_file="$tmp_dir/run.log"

printf '{"state":"staged"}\n' > "$manifest"
printf 'started_at=%s\n' "$(date --iso-8601=seconds)" > "$log_file"

stat --format='mode=%a owner=%U group=%G path=%n' "$tmp_dir" "$manifest" "$log_file"
Cleanup is not rollback

Removing a temporary directory restores scratch state. Undoing a package upgrade, database write, firewall change, or deployment requires a separate rollback design. Never imply transactional guarantees merely because a trap deletes temporary files.

5. Scale cleanup with a small registered-action stack

One fixed cleanup function is sufficient for many scripts. When resources are created conditionally, register cleanup actions only after each resource exists. Avoid eval; store functions and arguments in arrays or use narrowly defined cleanup functions.

declare -a cleanup_paths=()

register_path() {
  cleanup_paths+=("$1")
}

cleanup_paths_now() {
  local status=$? index
  trap - EXIT INT TERM

  for (( index=${#cleanup_paths[@]}-1; index>=0; index-- )); do
    rm -rf -- "${cleanup_paths[index]}" || true
  done
  exit "$status"
}

trap cleanup_paths_now EXIT
trap 'exit 130' INT
trap 'exit 143' TERM

stage_dir=$(mktemp -d)
register_path "$stage_dir"

cache_dir=$(mktemp -d)
register_path "$cache_dir"

printf 'stage=%s cache=%s\n' "$stage_dir" "$cache_dir"

Reverse-order cleanup mirrors resource acquisition: dependent resources are removed before the resources they depend on. Keep the registered type narrow; arbitrary command strings create quoting and injection hazards.

6. Hands-on lab: prove cleanup under injected failure

The lab creates a private workspace, records its path outside that workspace, injects either success or failure, and verifies that the directory is removed in both cases.

lab="$HOME/devops-academy/linux/chapter15/lesson01"
rm -rf "$lab"
mkdir -p "$lab"
cd "$lab"

cat > cleanup-demo.sh <<'SCRIPT'
#!/usr/bin/env bash
set -Eeuo pipefail

mode=${1:-success}
record_file=${2:?record file required}
work_dir=''

cleanup() {
  local status=$?
  trap - EXIT ERR INT TERM
  if [[ -n $work_dir ]]; then
    rm -rf -- "$work_dir" || true
  fi
  exit "$status"
}

on_error() {
  local status=$?
  printf 'failed status=%d line=%s command=%q\n' \
    "$status" "${BASH_LINENO[0]:-?}" "$BASH_COMMAND" >&2
  return "$status"
}

trap cleanup EXIT
trap on_error ERR
trap 'exit 130' INT
trap 'exit 143' TERM

umask 077
work_dir=$(mktemp -d -- "${TMPDIR:-/tmp}/cleanup-demo.XXXXXXXX")
printf '%s\n' "$work_dir" > "$record_file"
printf 'payload\n' > "$work_dir/data.txt"

case $mode in
  success) printf 'completed\n' ;;
  fail) false ;;
  *) printf 'unknown mode: %s\n' "$mode" >&2; exit 2 ;;
esac
SCRIPT

chmod u+x cleanup-demo.sh
bash -n cleanup-demo.sh

./cleanup-demo.sh success "$lab/success-path.txt"
success_path=$(<"$lab/success-path.txt")
[[ ! -e $success_path ]]

if ./cleanup-demo.sh fail "$lab/fail-path.txt"; then
  printf 'FAIL: injected failure unexpectedly succeeded\n' >&2
  exit 1
fi
fail_path=$(<"$lab/fail-path.txt")
[[ ! -e $fail_path ]]

printf 'PASS: both workspaces were removed\n' 

Verification checklist

7. Common reliability mistakes

“set -e means every error stops the script.”

Its behavior depends on grammatical context. Critical failures should be handled explicitly and regression-tested.

“An ERR trap is an exception handler.”

It shares many contextual exceptions with errexit and should be treated primarily as diagnostic instrumentation.

“rm -rf is safe inside an EXIT trap.”

Only after the path is proven non-empty, owned by the script, and created through a secure mechanism such as mktemp -d.

“Cleanup restores the system.”

Cleanup releases temporary resources. Rollback must be designed for each persistent mutation.

8. Knowledge check

Question 1. Why should an EXIT trap save $? immediately?

Question 2. What is the difference between pipefail and explicit pipeline recovery?

Question 3. Why is mktemp -d preferable to a path containing $$?

9. Summary

Reliable Bash uses options as documented policy, not folklore. Handle expected statuses explicitly, register cleanup before risky work, preserve the original exit status, keep traps conservative, create temporary resources atomically, and distinguish cleanup from rollback. Failure injection is the proof that the design works when the happy path does not.

10. Further reading

Next lesson

Parsing Options and Building Command-Line Interfaces

Define stable command-line contracts, parse arguments without evaluation, validate complete plans, and separate data from diagnostics.

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.