Bash / Intermediate: reliable data and failure paths / Lesson 9
Use traps for bounded lifecycle handling
Preserve status and cleanup invariants while recognizing signal timing, trap inheritance, and interruption limits.
On this page
Lesson goals
- Separate EXIT cleanup from ERR diagnostics.
- Preserve the triggering status through cleanup.
- Design idempotent partial-resource cleanup without universal guarantees.
A trap is shell code run at a lifecycle event
trap registers an action for selected signals or shell-defined events such as EXIT. The action is parsed and executed later, so quoting determines whether values are expanded during registration or during execution. Prefer a named cleanup function with clear state rather than a long interpolated command string. EXIT supports ordinary shell termination paths; it is not a guarantee against power failure or SIGKILL. Keep durable data safe independently of trap execution. A trap should complement an explicit operation contract, not become the only place where success or failure is decided.
Capture the incoming status before cleanup commands
The cleanup path can itself run commands that replace $?. Capture the incoming status immediately and decide whether cleanup failure should change the reported result. Often the original operation failure should remain primary while cleanup trouble is reported separately. A cleanup function should tolerate resources that were never created or were already released. Register cleanup after the relevant ownership state is established, and quote every owned path. This lesson uses only synthetic state and private temporary directories; it never removes arbitrary paths or relies on a trap to authorize a destructive operation.
bash -c 'cleanup() { status=$?; printf "exit-status=%s\n" "$status"; }; trap cleanup EXIT; exit 7'
child_status=$?
printf 'observed=%s\n' "$child_status"
exit-status=7
observed=7
A disposable child exits seven and its EXIT handler prints the saved value. The parent observes seven and ends successfully after reporting the controlled experiment.
Signal delivery and shell waiting affect timing
A signal trap does not mean the action interrupts every external command at an arbitrary instant. Bash has documented behavior while waiting for foreground commands and while using wait for asynchronous work. A trapped signal can change the wait result, and child coordination still needs an explicit policy. SIGKILL and SIGSTOP cannot be handled by shell traps. Cancellation should identify owned children, request the appropriate documented action, allow bounded completion, and preserve status. Never target guessed PIDs or broad process names; advanced job lessons restrict demonstrations to children created by the example.
ERR follows exclusions and inheritance rules
ERR is a Bash event with conditions related to errexit exclusions. A failure tested by if, negated with !, or placed in certain lists and pipelines may not trigger the assumed diagnostic path. Function and substitution inheritance depend on options such as errtrace and context. Therefore an ERR handler is useful supporting observability, not a universal exception system. Keep explicit handling around required operations and test the actual caller structure. Traces and diagnostics can expose secrets, so avoid dumping all variables or expanded commands merely because a failure occurred.
Test partial progress and repeated cleanup
A lifecycle design should survive failure before allocation, after allocation, and after successful publication. Track which resources the script actually owns and separate temporary state from the only copy of important content. Idempotent cleanup means a repeated request does not create another error or affect unrelated state. Prefer private temporary names from the platform’s supported tool rather than predictable shared filenames. Verify success and failure paths with harmless markers before introducing real resources. The example below demonstrates a repeatable state transition only; deleting real data requires an independent reviewed operation design.
released=0
cleanup() {
if (( released == 0 )); then
released=1
printf 'released\n'
fi
}
cleanup
cleanup
printf 'state=%s\n' "$released"
released
state=1
The second call has no additional effect. This verifies the ownership-state pattern without deleting files or signaling another process.
Common errors and fixes
Cleanup reports the logger’s status
- Why it happens
- The original $? was overwritten first.
- How to fix it
- Save the incoming status before any cleanup command.
- Prevent it next time
- Test a controlled nonzero exit through the handler.
A trap is treated as crash-proof storage protection
- Why it happens
- Uncatchable signals and machine failure were ignored.
- How to fix it
- Preserve independent durable copies and publication rules.
- Prevent it next time
- Document what cleanup cannot guarantee.
ERR is expected for every nonzero command
- Why it happens
- Grammar and inheritance exclusions were not considered.
- How to fix it
- Handle required work explicitly and test call contexts.
- Prevent it next time
- Use ERR as supporting diagnostics only.
Check your understanding
1. What must cleanup do if allocation failed before ownership was recorded?
Show worked solution
Recognize that no owned resource exists and return without affecting unrelated state. Cleanup should use explicit initialization and ownership flags rather than guess a default path. Test this path independently from successful allocation.
2. Why can a tested failing command skip an expected ERR message?
Show worked solution
ERR obeys documented control-flow exclusions. The failure is being consumed by a condition or another listed context. Put the required failure decision in that branch and treat trap output as supplementary evidence, not the sole error mechanism.
Key takeaways
- Preserve status and ownership before cleanup work.
- Test traps within their actual signal and call context.