Write scripts that fail safely: read exit codes, stop on errors with set -euo pipefail, clean up with trap, and log failures so you can diagnose what went wrong.
Why: every command finishes with an exit code — 0 means success, anything else means a specific failure. $? holds the last one. Your own script should exit 0 when it worked and a non-zero code when it did not, so callers and CI can react.
#!/usr/bin/env bash
if ! cp source.txt dest.txt; then
echo "copy failed" >&2
exit 1 # signal failure to whoever called us
fi
echo "copied OK"
exit 0Why: by default a script keeps running even after a command fails, often making things worse. set -e tells Bash to exit the moment any command fails — so a failed step halts the script instead of barrelling on.
#!/usr/bin/env bash
set -e # exit immediately if a command fails
cd /opt/app # if this fails, the script stops here...
rm -rf build # ...and never runs this on the wrong directory
echo "cleaned"Why: the standard hardening line at the top of serious scripts. -e exits on error, -u treats use of an unset variable as an error (catches typos), and -o pipefail makes a pipeline fail if ANY stage fails — not just the last. Note: combine them as set -euo pipefail.
#!/usr/bin/env bash
set -euo pipefail
# -u: a typo like $hsot is now a hard error, not an empty string
host="example.com"
echo "pinging $host"
# -o pipefail: this fails if 'curl' fails, even though 'grep' runs
curl -s "https://$host" | grep -q "ok"Why: trap runs a command when the script exits or is interrupted — the reliable way to remove temp files or release locks no matter how the script ends. EXIT fires on any exit; INT TERM catch Ctrl-C and kill signals.
#!/usr/bin/env bash
set -euo pipefail
workdir=$(mktemp -d) # temp directory
trap 'rm -rf "$workdir"' EXIT # always clean it up
echo "working in $workdir"
# ...do work; $workdir is removed automatically on exitWhy: a failure you cannot see is hard to fix. Route error messages to stderr with >&2, optionally tee them to a log file, and stamp them with the time. A small log() helper keeps this consistent across the script.
#!/usr/bin/env bash
set -euo pipefail
log() {
echo "[$(date +%T)] $*" >&2 # timestamped, to stderr
}
log "starting backup"
if ! tar -czf backup.tgz ./data 2>> errors.log; then
log "backup FAILED — see errors.log"
exit 1
fi
log "backup done"