Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 60

Bash Error Handling with Exit Codes and Traps

Recommended resources

Hello! Welcome back to your Bash scripting course.

In our last lesson, we explored awk, a powerful tool for parsing and processing column-based data. You learned to extract fields, filter lines, perform calculations, and generate formatted reports—all essential skills for analyzing logs and system outputs.

So far, we've focused on what to do when our scripts run correctly. But in the real world, things go wrong: files are missing, servers are unreachable, commands fail. A script that fails silently or continues running with bad data can be dangerous. As a DevOps engineer, the reliability of your automation is paramount. This lesson is about making your scripts robust and predictable.

Today's Learning Outcome: By the end of this lesson, you will be able to implement error handling using exit codes and trap commands in Bash.


1. The Foundation of Error Handling: Exit Codes

Every command you run in Linux finishes with an exit code (or exit status), which is an integer value that signals how it terminated. This is the most fundamental mechanism for error checking in shell scripting. The convention is simple:

  • An exit code of 0 means the command completed successfully.
  • Any non-zero exit code means the command failed.

Different non-zero values can signify different types of errors (e.g., 1 for a general error, 126 for a command not being executable, 127 for a command not being found).

Bash stores the exit code of the most recently executed command in a special variable: $?. You can check this value to determine if a command succeeded or failed.

Let's see it in action. In your terminal, run a command that you know will succeed, and then one that will fail:

# This will succeed
ls /etc/passwd
echo "Exit code: $?"

# This will fail
ls /nonexistent/file
echo "Exit code: $?"

You'll see an exit code of 0 for the first command and a non-zero code (likely 2 on most systems) for the second.

We can use this in an if statement to build simple error handling logic. It's also a best practice for your own scripts to terminate with a non-zero exit code when they fail, using the exit command.

Try-Catch in Bash: Bash Script Error Handling

This article, 'Try-Catch in Bash', begins by explaining the core concept of using the $? variable to check the exit status of a command. This is the manual but fundamental way to handle errors.

Read the first section, 'Basic Error Checking with Exit Status'. Focus on how the $? variable is used with an if statement to detect an error and how exit 1 is used to terminate the script.

Here's a typical example in a script. This script tries to create a directory and will stop if it fails.

#!/bin/bash

# Attempt to create a directory where we don't have permission
mkdir /root/test_dir

if [ $? -ne 0 ]; then
  echo "Error: Failed to create directory. Please check permissions."
  # Exit the script with a failure code
  exit 1
fi

# This line will only be reached if the mkdir command was successful
echo "Directory created successfully."

Checking $? after every command gives you full control, but it can make your scripts verbose and repetitive.



2. A Simpler Approach: Automatic Exits with set -e

Manually checking $? after every critical command is tedious. To simplify this, Bash provides a "fail-fast" mechanism using the set -e option (also written as set -o errexit).

When you include set -e at the top of your script, the shell will automatically exit if any command fails (returns a non-zero exit code).

This dramatically cleans up our scripts. Let's look at the same example from before, but this time using set -e.

Try-Catch in Bash: Bash Script Error Handling

The same article provides a cleaner solution to manual checks. Let's see how set -e simplifies error handling.

Read the second section, 'Using ‘set -e’ to Exit on Error'. Notice how much shorter and cleaner the example script becomes.

Here's the refactored script. It behaves identically to the previous one but is far more readable.

#!/bin/bash
set -e

# The script will exit here if mkdir fails
mkdir /root/test_dir

# This line is only reached if mkdir succeeds
echo "Directory created successfully."

While set -e is extremely useful, it's not a silver bullet. It has some nuanced behaviors, especially within if conditions or command pipelines (e.g., command1 | command2), where a failure might not cause an immediate exit. For this reason, while set -e is a great default for many scripts, more complex situations require a more robust solution.



3. Centralized and Flexible Handling with trap

The trap command is a powerful Bash built-in that allows you to "trap" signals or events and execute code when they occur. It gives you the flexibility of set -e's automatic detection but with the control of a manual check, allowing you to run custom logic—like logging an error or cleaning up temporary files—before exiting.

Two common uses for trap in scripting are:

  1. trap '...' ERR: Executes a command or function whenever a command in the script fails. This is your centralized error handler.
  2. trap '...' EXIT: Executes a command or function when the script exits, for any reason (success, failure, or user interruption). This is perfect for cleanup tasks that must always run.

Let's explore how to use trap for powerful, centralized error handling.

Try-Catch in Bash: Bash Script Error Handling

Now we'll look at the most flexible method. These sections from the article introduce the trap command and a very common pattern: using it to call a custom error-handling function.

Read sections 3 and 5, 'Error Handling with Traps' and 'Custom Error Handling Function'. Pay close attention to: The syntax: trap '...' ERR. The use of a function to hold the error-handling logic. The special $LINENO variable, which provides the line number where the error occurred.

A common and highly effective pattern in professional scripts is to combine trap with a function for detailed logging. You can also add set -o errtrace, which ensures that your trap is triggered inside functions and subshells, making it more reliable.

Here is a robust script template you can adapt for your own projects:

#!/bin/bash

# Exit immediately if a command exits with a non-zero status.
set -o errexit
# Ensure the error trap is inherited by functions and subshells.
set -o errtrace

# Define the error handling function
error_handler() {
  local exit_code=$?
  local line_number=$1
  echo "ERROR on line ${line_number}: Command exited with status ${exit_code}."
  # You could add logging, send a notification, etc. here.
}

# Set the trap to call the error handler on any error
trap 'error_handler $LINENO' ERR

# --- Main script logic starts here ---
echo "Creating a temporary file..."
TEMP_FILE="/tmp/myscript.$$.tmp"
touch "$TEMP_FILE"

echo "Temporary file created at $TEMP_FILE"

echo "Attempting to read from a non-existent file..."
cat /path/to/nonexistent_file

# This part of the script will not be reached
echo "Script finished successfully."
rm "$TEMP_FILE"

When you run this script, it will fail on the cat command, and your trap will trigger the error_handler function, printing a useful, context-rich error message before the script exits.

Bash Script Error Handling with 'trap ERR'
This image shows the output of a script using `trap ERR`. Notice how it prints a custom, informative error message—including the line number and exit code—in addition to the standard system error. This is invaluable for debugging.
Test your understanding!

Consider the following script, which is supposed to create a backup of the /var/log directory.

#!/bin/bash

# Script to back up /var/log

BACKUP_DIR="/mnt/backups"
FILENAME="log_backup_$(date +%Y-%m-%d).tar.gz"

echo "Starting backup..."
# This command will fail because /mnt/backups likely doesn't exist or isn't writable
tar -czf "$BACKUP_DIR/$FILENAME" /var/log

echo "Backup created at $BACKUP_DIR/$FILENAME"
echo "Script finished."
  1. How would you modify this script using set -e to ensure it stops immediately if the tar command fails?
  2. Instead of set -e, how would you modify the script to use trap to print the message "Backup failed!" and then exit?
Show answer
  1. Using set -e: Simply add set -e to the top of the script. The script will halt at the tar command, and "Backup created..." will not be printed.

    #!/bin/bash
    set -e 
    
    # Script to back up /var/log
    
    BACKUP_DIR="/mnt/backups"
    FILENAME="log_backup_$(date +%Y-%m-%d).tar.gz"
    
    echo "Starting backup..."
    tar -czf "$BACKUP_DIR/$FILENAME" /var/log
    
    echo "Backup created at $BACKUP_DIR/$FILENAME"
    echo "Script finished."
    
  2. Using trap: Add trap 'echo "Backup failed!"; exit 1' ERR to the top. This will catch the error from tar, print the custom message, and then exit with a failure code.

    #!/bin/bash
    
    trap 'echo "Backup failed!"; exit 1' ERR
    
    # Script to back up /var/log
    
    BACKUP_DIR="/mnt/backups"
    FILENAME="log_backup_$(date +%Y-%m-%d).tar.gz"
    
    echo "Starting backup..."
    tar -czf "$BACKUP_DIR/$FILENAME" /var/log
    
    echo "Backup created at $BACKUP_DIR/$FILENAME"
    echo "Script finished."
    


Conclusion

You have now learned the three primary techniques for building robust and reliable Bash scripts. By moving from manual checks to automated, flexible error handling, you can write automation that you and your team can trust.

Key Takeaways:

  • Exit Codes ($?): The foundation of error handling. 0 for success, non-zero for failure. Check $? for manual control.
  • Fail-Fast (set -e): A simple and effective way to make a script exit immediately upon any error. It's a great default for most scripts.
  • Centralized Handling (trap): The most powerful and flexible method. Use trap '...' ERR to run custom error-logging or cleanup functions, and trap '...' EXIT for actions that must always run before the script terminates.

In our next lesson, we will focus on another key aspect of writing professional scripts: organization and reusability. You will learn how to create reusable functions in Bash scripts to organize code, making your scripts cleaner, easier to maintain, and simpler to test.