trap, Cleanup, and Signal-Aware Scripts
Resources acquired by a script need a lifecycle. Traps connect shell exit and signals to cleanup logic, but they work best when status preservation, child ownership, and untrappable failure are part of the design.
Learning objectives
By the end of this lesson
- Register reliable EXIT cleanup.
- Preserve failure status during cleanup.
- Separate signal response from final cleanup.
- Terminate owned child processes.
- Design for failures that bypass traps.
1. trap binds cleanup logic to lifecycle events
cleanup() {
printf 'cleanup\n' >&2
}
trap cleanup EXITThe EXIT pseudo-signal is the common foundation for temporary-resource cleanup. Signal traps such as TERM and INT can add graceful termination behavior.
2. Preserve the incoming exit status
cleanup() {
local status=$?
rm -f -- "${tmp:-}"
exit "$status"
}
trap cleanup EXITWithout saving the incoming status first, cleanup commands can replace the status that caused the exit.
3. Register cleanup immediately after acquisition
tmpdir=$(mktemp -d) || exit 1
trap 'rm -rf -- "$tmpdir"' EXITThe earlier cleanup is registered, the fewer later error paths can leak the resource.
4. Cleanup functions are easier to maintain than dense trap strings
tmpdir=""
child_pid=""
cleanup() {
local status=$?
[[ -n $child_pid ]] &&
kill -TERM "$child_pid" 2>/dev/null || true
[[ -n $tmpdir && -d $tmpdir ]] &&
rm -rf -- "$tmpdir"
exit "$status"
}
trap cleanup EXITFunctions provide normal local variables, branches, comments, and testing opportunities.
5. Separate signal response from final cleanup
on_term() {
printf 'termination requested\n' >&2
exit 143
}
trap on_term TERM INT
trap cleanup EXITThe signal handler decides why to exit; the EXIT handler performs shared cleanup once.
6. Owned child processes belong in cleanup policy
child_pid=""
cleanup() {
local status=$?
if [[ -n $child_pid ]]; then
kill -TERM "$child_pid" 2>/dev/null || true
wait "$child_pid" 2>/dev/null || true
fi
exit "$status"
}This avoids leaving background work running after the parent exits unexpectedly.
7. Cleanup must tolerate partial initialization
cleanup() {
local status=$?
if [[ -n ${tmpdir:-} && -d $tmpdir ]]; then
rm -rf -- "$tmpdir"
fi
if [[ -n ${lockfile:-} && -e $lockfile ]]; then
rm -f -- "$lockfile"
fi
exit "$status"
}A script may fail before every resource exists. Cleanup should safely handle that state.
8. ERR traps are not a universal exception hook
trap 'printf "ERR status=%d command=%s\n" "$?" "$BASH_COMMAND" >&2' ERRERR has context rules related to errexit, functions, subshells, and conditionals. It is useful for diagnostics but should not replace explicit error paths.
9. RETURN and DEBUG traps are specialized tools
Bash also supports traps such as DEBUG and RETURN. They are useful for tracing and instrumentation but add complexity and should not be part of ordinary application logic without a clear reason.
10. Cleanup is best effort, not guaranteed
SIGKILL, power loss, kernel failure, or runtime termination can bypass traps completely.
Locks, temp files, and checkpoints should tolerate stale state. Cleanup is a convenience and safety layer, not an absolute guarantee.
11. Hands-on lab: signal-aware temporary workspace
mkdir -p "$HOME/devops-academy/bash/chapter11/lesson04"
cd "$HOME/devops-academy/bash/chapter11/lesson04"
cat > worker.sh <<'EOF'
#!/usr/bin/env bash
set -u
tmpdir=$(mktemp -d) || exit 1
child_pid=""
cleanup() {
local status=$?
if [[ -n $child_pid ]]; then
kill -TERM "$child_pid" 2>/dev/null || true
wait "$child_pid" 2>/dev/null || true
fi
rm -rf -- "$tmpdir"
exit "$status"
}
on_term() {
printf 'termination requested\n' >&2
exit 143
}
trap cleanup EXIT
trap on_term TERM INT
bash -c 'while :; do sleep 1; done' &
child_pid=$!
printf 'tmpdir=%s child=%s parent=%s\n' "$tmpdir" "$child_pid" "$$"
wait "$child_pid"
EOF
chmod u+x worker.sh
printf 'Run ./worker.sh, then send TERM to its parent PID to test cleanup.\n'Verification checklist
12. Knowledge check
Question 1. Why save $? at the beginning of cleanup?
Question 2. Why should cleanup be idempotent?
Question 3. Can an EXIT trap handle SIGKILL?
Question 4. Should ERR be treated as a universal exception mechanism?
13. Summary
Traps connect resource cleanup and process shutdown to shell lifecycle. Preserve status, register cleanup early, track child processes, separate signal response from shared cleanup, and design recovery for the cases where traps never run.
14. Further reading
- GNU Bash Reference Manual — Traps and Signals.
- GNU Bash Reference Manual —
ERR,DEBUG, andRETURN. - Linux
signal(7). - ShellCheck documentation — trap and cleanup patterns.
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this address.