Build the mental model
Every command that finishes running sets a numeric exit status between 0 and 255, stored in the special variable $?, following a Unix convention: 0 means success, and any nonzero value signals some kind of failure — though the specific nonzero number's meaning is up to the program (1 is a generic catch-all, 127 usually means 'command not found', 130 means killed by SIGINT). $? gets overwritten by the very next command that runs, so if you want to inspect it you have to capture it immediately. Left unchecked, a script has no built-in protection against failure: it happily keeps executing after a failed command, silently carrying a bad state forward. The strict-mode trio turns that silence into noise. set -e stops the script the instant any command exits nonzero — but with real exceptions worth knowing up front: it does NOT fire for a command used as the test in an if/while/until, or on the left side of && or ||, because those constructs are explicitly testing for failure and are expected to handle it themselves. set -u turns a reference to an unset variable into a hard error instead of silently expanding to an empty string — which matters in something like rm -rf "$TARGET_DIR/" "$OLD_DIR/", where a typo'd or unset $OLD_DIR would otherwise quietly vanish and turn the command into something far more dangerous. set -o pipefail makes a pipeline's exit status reflect ANY stage failing, not just the last — without it, false | true reports success, since bash only checks true's status by default. Together, set -euo pipefail at the top of a script is the closest thing bash has to 'fail fast and loudly'.
Connect it to a real scenario
Picture a deployment script that runs a database migration and then restarts the app server: two commands, one after another, with real consequences if they run out of order. Without strict mode, if the migration command fails — say a SQL file has a syntax error — bash's default behavior is to print whatever error message the failing command produced and move right along to the next line, which restarts the app server anyway. Now you're in a worse spot than before: a service is running against a half-migrated database, potentially serving corrupted data or crashing on missing columns, and nothing about the deploy log makes that obvious unless someone reads it carefully afterward. Adding set -euo pipefail at the very top of the script changes this completely: the moment the migration command returns a nonzero exit status, the script halts right there, before the restart command even runs. The failure is loud (bash reports the failing line and command) and immediate, so whoever is watching the deploy — a person or a CI pipeline — sees a clear failure instead of a deploy that appeared to succeed while quietly leaving the system broken. This is exactly the trade the strict-mode trio makes: a script that might stop more often, in exchange for never silently continuing past a state it can't recover from.
Try the working example
#!/bin/bash
set -euo pipefail
echo "Running a command that succeeds..."
true
echo "Exit status of last command: $?"
echo "Running a command that fails, but caught with ||..."
false || echo "Caught with ||, exit status of false was: $?"
echo "--- set -e gotcha: a failing command inside an if condition does NOT stop the script ---"
if false; then
echo "this branch does not run"
else
echo "if caught the failure itself, script keeps going"
fi
echo "--- pipefail demo: without it, a failing command hidden earlier in a pipe is invisible ---"
if false | true; then
echo "pipeline looked like success"
else
echo "pipefail caught the failing 'false' even though 'true' ran last, exit status: $?"
fi
set +e
false
echo "set +e disabled strict mode, so we reached here. Exit status of false was: $?"
Running the script above prints:
Running a command that succeeds...
Exit status of last command: 0
Running a command that fails, but caught with ||...
Caught with ||, exit status of false was: 1
--- set -e gotcha: a failing command inside an if condition does NOT stop the script ---
if caught the failure itself, script keeps going
--- pipefail demo: without it, a failing command hidden earlier in a pipe is invisible ---
pipefail caught the failing 'false' even though 'true' ran last, exit status: 1
set +e disabled strict mode, so we reached here. Exit status of false was: 15-minute try-it
Write a script with set -euo pipefail that copies a file to a backup location and then greps for a word inside it. Deliberately misspell the source filename, run the script, and observe that it stops immediately instead of running the grep against a nonexistent backup.
One important caution
Assuming set -e catches every failure — it silently does NOT fire for commands inside if/while conditions, or on the left side of && and ||, because those contexts are expected to test for failure
Using set -u before all variables have defaults, which crashes scripts on legitimately optional variables like ${1:-} — use ${VAR:-default} to give unset variables a safe fallback
GNU Bash Manual — The Set Builtin — Bash / Shell Scripting