Introduction
Hello! In our last lesson, you built a flexible cleanup_advanced.sh script that could be configured from the command line using getopts. This was a major step towards creating professional, reusable tools. However, what happens if a user provides an invalid path for the archive directory, or if a command within the script fails unexpectedly? Currently, the script might stop with a cryptic error or, worse, fail silently, leaving you thinking the job was done when it wasn't.
This lesson directly addresses that problem. Today, we'll learn how to handle errors and exit codes to make your scripts robust. A robust script is one that behaves predictably, reports failures clearly, and prevents unintended consequences. For your goal of becoming a Full Stack Developer, this is non-negotiable. When you deploy a Django application, you need your automation scripts to be completely reliable; you must know immediately and clearly if a step like a database migration or a file copy has failed.
By the end of this lesson, you will be able to write scripts that can anticipate problems and stop safely, making them trustworthy enough for real-world system administration and deployment tasks.
1. The Language of Commands: Exit Codes
Every command you run in the terminal, whether it succeeds or fails, reports its status back to the shell using a number called an exit code. By convention:
- An exit code of
0means the command was successful. - Any non-zero exit code (1, 2, 127, etc.) means the command failed.
Bash stores the exit code of the most recently executed command in a special variable: $?.
Let's watch a short video that explains this fundamental concept.
How To Write Bash Scripts In Linux - Complete Guide (Part 6 - Exit Codes)
This segment from the "How To Write Bash Scripts In Linux" series by Learn Linux TV clearly explains what exit codes are and demonstrates how to view them using echo $?.
Watch from the beginning to 04:07. Focus on: The difference between human-readable errors and the exit code. How the $? variable is used to see the exit code of the last command. The core rule: 0 is success, and anything else is failure.
To see this in action on your own system, open your terminal and try these commands:
# This command should succeed
ls /etc/os-release
echo $?
# Expected output: 0
# This command should fail
ls /nonexistent/directory
echo $?
# Expected output: A non-zero number, like 2

This mechanism is the foundation of error handling in shell scripting. An automated script, like a backup or deployment script running overnight, can't "see" an error message on the screen. But it can check the exit code to know with certainty whether a critical step succeeded or failed.
2. Taking Action: Checking Exit Codes and Exiting
Knowing an error occurred is the first step. The next is deciding what to do about it. You can use if statements to check the value of $? and control your script's flow. However, there's a critical rule: you must check $? immediately after the command you're interested in. Any other command, even a simple echo, will run successfully and overwrite $? with 0, masking the original error.
You can also use the exit command to immediately terminate your script and return a specific exit code. This is useful for signaling to other scripts or monitoring tools that your script has failed.
This video demonstrates both checking $? in an if block and using the exit command. Pay close attention to the common pitfall explained partway through.
How To Write Bash Scripts In Linux - Complete Guide (Part 6 - Exit Codes)
Let's continue with the Learn Linux TV video. This part shows how to use if statements to handle different outcomes and highlights the critical mistake of not checking $? immediately.
Watch the segments from 05:30 to 11:16 and from 14:43 to 20:40. Focus on: (05:30 - 11:16) How an if statement can check if $? is equal to 0 to run different blocks of code. (14:43 - 17:00) This part is crucial. Understand why an echo statement between a failed command and checking $? can hide the error. (17:00 - 20:40) How the exit command gives you explicit control to stop the script and set its final exit code.
A more direct way to check a command's success in an if statement is to put the command itself in the condition. Bash treats a 0 exit code as "true" and a non-zero exit code as "false".
if cp source.txt destination.txt; then
echo "File copied successfully."
else
echo "Error: Could not copy the file." >&2 # Redirect error message to stderr
exit 1 # Exit the script with a failure code
fi
This is cleaner and avoids the $? pitfall.
3. Fail Fast: Automating Error Handling with set
Checking every single command with an if statement can make your scripts long and cluttered. For many scripts, a simpler and safer approach is to adopt a "fail fast" policy: if any command fails, the script should stop immediately.
You can configure this behavior using the set command at the top of your script. This is often called "strict mode".
set -e
set -u
set -o pipefail
Let's watch a video that explains what each of these options does.
bash "strict mode": -euo pipefail (beginner - intermediate) anthony explains #201
This video from anthonywritescode provides a fast-paced and clear explanation of the three most important set options for writing safe Bash scripts.
Watch from 03:07 to 09:27. The video covers the options one by one: set -e (or set -o errexit): Exits the script immediately if any command fails (returns a non-zero exit code). set -u (or set -o nounset): Treats using an undefined variable as an error and exits. This helps catch typos in variable names. set -o pipefail: Makes a pipeline's exit code the code of the rightmost command to exit with a non-zero status, instead of just the last command. This prevents errors from being hidden inside a pipeline.

A common best practice is to start every script with set -euo pipefail. This single line dramatically increases the safety and reliability of your scripts. You can read a quick summary of these essential techniques in the following article.
Bulletproof Bash Scripts: Mastering Error Handling
This article, "Bulletproof Bash Scripts," reinforces the 'fail fast' principle. We'll just focus on the first part that summarizes the set options and introduces the trap command, which we'll use in our exercise.
Read the section 'Essential Bash Error Handling Techniques Every Developer Should Know'. Focus on the three subsections: 'Use set options to fail fast', 'Check exit codes explicitly', and 'Use trap to handle script termination'. This will give you a solid textual reference for these concepts.
Do-It-Yourself: Creating a Robust Cleanup Script
It's time to apply these techniques to make your cleanup_advanced.sh script truly robust. You will create a new script, cleanup_robust.sh, that can handle errors gracefully.
Your Goal: Modify the script so it stops immediately if something goes wrong, like being unable to create a directory, and that it performs a cleanup action on exit.
Instructions:
-
Create the new script:
In your~/automation_practicedirectory, copy your previous script:cp cleanup_advanced.sh cleanup_robust.sh -
Enable "Strict Mode":
At the top ofcleanup_robust.sh, right after the#!/usr/bin/env bashline, add the following:set -euo pipefail -
Add a
trapfor Graceful Exits:
Atrapcatches a signal (likeEXITorINTfor Ctrl+C) and runs a command. This is perfect for cleanup. Add a simple trap function and register it.# Place this function definition near the top with your other functions cleanup() { echo "Cleanup script finished or was interrupted. Exiting." } # Place this command right after the set -euo pipefail line trap cleanup EXITNow, no matter how your script exits (success, error, or Ctrl+C), it will print this final message.
-
Make
setup_archive_dirRobust:
Your currentsetup_archive_dirfunction doesn't check ifmkdirsucceeded. Withset -e, the script would exit ifmkdirfailed, but the user wouldn't know why. Let's add an explicit check for a clearer error message.
Modify the function to look like this:setup_archive_dir() { local path="$1" if [[ ! -d "$path" ]]; then echo "Archive directory '$path' not found, creating it." # The -p option is still useful, as it creates parent directories if ! mkdir -p "$path"; then echo "Error: Failed to create archive directory '$path'." >&2 # With 'set -e', the script would exit here anyway, # but an explicit 'exit 1' makes the intent clear. exit 1 fi fi } -
Test Your Robust Script:
First, let's create a situation wheremkdirwill fail. We'll create a file namedarchive, so the script can't create a directory with the same name.cd ~/automation_practice # Clean up old dirs and create a file to cause an error rm -rf archive backups touch archive # Run the script. It should now fail with your clear error message. ./cleanup_robust.sh "*.log" "*.tmp"You should see the "Failed to create archive directory" message, followed by the "Exiting" message from your
trap.Now, test the successful path:
# Clean up and create fresh test files rm -rf archive touch app-1.log app-2.log data.csv # Run the script. It should work as before. ./cleanup_robust.sh "*.log" ls archive # Should contain the log filesYou will see the "Exiting" message from
trapat the end, confirming it runs on successful completion too.
Click here to see the complete `cleanup_robust.sh` script.
#!/usr/bin/env bash
set -euo pipefail
# --- Trap for cleanup on exit ---
trap cleanup EXIT
# --- Functions ---
cleanup() {
# This function is called when the script exits.
echo "Cleanup script finished or was interrupted. Exiting."
}
setup_archive_dir() {
local path="$1"
if [[ ! -d "$path" ]]; then
echo "Archive directory '$path' not found, creating it."
if ! mkdir -p "$path"; then
echo "Error: Failed to create archive directory '$path'." >&2
exit 1
fi
fi
}
archive_files() {
local pattern="$1"
local archive_path="$2"
local files_found=0
# Use find to locate files and check if any were found
while IFS= read -r file; do
if [[ -f "$file" ]]; then
echo "Archiving '$file'..."
cp "$file" "$archive_path/"
rm "$file"
files_found=1
fi
done < <(find . -maxdepth 1 -type f -name "$pattern")
if [[ "$files_found" -eq 0 ]]; then
echo "No files found matching pattern: $pattern"
fi
}
usage() {
echo "Usage: $0 [-a ARCHIVE_DIR] [-p PATTERN] [PATTERN...]"
echo " -a ARCHIVE_DIR Specify the archive directory (default: archive)."
echo " -p PATTERN Specify a file pattern to archive (can be used multiple times)."
echo " -h Display this help message."
echo "Any additional arguments are also treated as patterns."
}
# --- Default values ---
ARCHIVE_DIR="archive"
PATTERNS_TO_ARCHIVE=()
# --- Parse Options ---
while getopts "a:p:h" opt; do
case ${opt} in
a)
ARCHIVE_DIR="$OPTARG"
;;
p)
PATTERNS_TO_ARCHIVE+=("$OPTARG")
;;
h)
usage
exit 0
;;
\?)
echo "Invalid Option: -$OPTARG" 1>&2
usage
exit 1
;;
esac
done
# --- Handle Remaining Arguments ---
shift $((OPTIND - 1))
if [[ $# -gt 0 ]]; then
PATTERNS_TO_ARCHIVE+=("$@")
fi
# --- Validation ---
if [[ ${#PATTERNS_TO_ARCHIVE[@]} -eq 0 ]]; then
echo "Error: No file patterns specified." >&2
usage
exit 1
fi
# --- Main Logic ---
TARGET_DIR="."
FULL_ARCHIVE_PATH="$TARGET_DIR/$ARCHIVE_DIR"
setup_archive_dir "$FULL_ARCHIVE_PATH"
for pattern in "${PATTERNS_TO_ARCHIVE[@]}"; do
archive_files "$pattern" "$FULL_ARCHIVE_PATH"
done
echo "Cleanup complete."
Conclusion
Fantastic work today! You have taken a huge leap forward in writing professional-grade Bash scripts. By understanding and handling errors, you've transformed your script from a simple tool into a reliable piece of automation that you can trust.
Key Takeaways:
- An exit code of
0means success; any non-zero value means failure. Check the last exit code with$?. - You can check exit codes with
ifstatements for fine-grained control or use theexitcommand to terminate the script. - The "fail fast" method using
set -euo pipefailis the recommended best practice for making scripts automatically robust. - The
trapcommand is a powerful tool for ensuring cleanup actions (like removing temporary files) run no matter how a script exits.
In our next lesson, we will put everything you've learned so far together. We'll write a practical automation script to back up a specified project directory. This script will use functions, parse command-line arguments, and incorporate the robust error handling you learned today to create a tool suitable for real-world use.