Each and every command you execute in Linux will return a number called an exit status code, and it shows whether your Bash script has succeeded or failed. Mastering bash script exit codes and error handling is the difference between beginner scripts that silently crash and professional scripts that fail quickly, provide clear logging, and can automatically restart after errors. The purpose of this article is to help you learn how Bash script exit codes function, how they should be used, and how you can implement real-time error checking within your Bash scripts using a step-by-step approach.
Why Exit Codes Matter in Real Scripts:
If you don't properly handle your exit codes when using bash scripts, they could be failing without causing an error message, which leaves a system broken during the middle of execution, it may leave a deployment process partially complete, or it will never make a backup
- A deployment script that ignores a failed
git pullwill overwrite production with bad code - A backup script without error checks will appear to succeed even when the archive is corrupt
- A cron job with no exit code logging gives you zero visibility when things go wrong at 2am
Understanding bash exit codes isn't optional - it's the foundation of safe automation.
If you are new to shell scripting, take a look at our Bash Scripting Basics for Beginners Guide as this provides a basic foundation for all of the syntax you will require for dealing with exit codes.
What Are Bash Script Exit Codes and Error Handling? (Complete Reference)
An exit code (also called status or return code) is a number between 0 and 255 that every Linux command returns when it has finished. 0 always means success. Anything other than zero tells you something went wrong. Most of the time, the actual number provides very specific information on what went wrong.
Exit Code Reference Table:
Here are the standard bash exit codes you will encounter most often. Understanding each one, especially 126, 127 & 130 will save you hours of debugging. See our Linux bash cheat sheet for a printable version.
0- Success: the command completed without errors1- General error: catch-all for unspecified failures2- Misuse of shell built-ins (wrong syntax, missing arguments)126- Permission denied: file found but not executable127- Command not found: typo or missing from PATH128- Invalid argument passed to theexitcommand128+N- Script killed by signal N (e.g. 130 = killed by Ctrl+C, signal 2)130- Script terminated with Ctrl+C (SIGINT)255- Exit status out of range
Here's how to see an exit code in action. Run a valid command, then a broken one, and compare:
LinuxTeck.com
echo $?
ls /nonexistent_dir
echo $?
0
ls: cannot access '/nonexistent_dir': No such file or directory
2
Common Mistake:
The $? variable holds the exit code of only the last executed command. If you run any other command between your target command and echo $? - even an echo, you will overwrite it and lose the original exit code.
Fix: Capture it immediately with exit_code=$? and then use $exit_code wherever you need it.
How to Check Exit Status with $? in Bash Scripts
The special variable $? is your window into exit codes. Every time a command runs, bash updates $? with that command's exit code. Here is how to use it practically inside a script, not just at the terminal.
LinuxTeck.com
mkdir /tmp/myproject
status=$?
if [ $status -eq 0 ]; then
echo "Directory created successfully."
else
echo "ERROR: Failed to create directory. Exit code: $status"
exit $status
fi
ERROR: Failed to create directory. Exit code: 1
Pro Tip - Shorthand with || (OR operator):
For simple one-liners, you can use the || operator instead of a full if-block. It runs the right side only when the left side fails, which is perfect for quick exit-on-error patterns.
LinuxTeck.com
For more conditional logic patterns, see our article on bash if else examples with real use cases.
The exit Command: Terminate Your Script with a Meaningful Code
The exit command does two things: it stops script execution immediately and passes a numeric code back to the calling shell or process. Using exit well makes your scripts composable. Other scripts and CI/CD pipelines can reliably detect success or failure.
LinuxTeck.com
# Good practice: use meaningful exit codes
exit 0 # success
exit 1 # general error
exit 2 # misuse / wrong arguments
exit 127 # command not found (use when checking dependencies)
Real-world pattern - argument validation:
Use exit 2 when a script is called with wrong or missing arguments - this mirrors the convention used by bash built-ins themselves and makes your scripts easier to debug in pipelines. Check out our guide to handling arguments in bash scripts for more patterns.
LinuxTeck.com
if [ $# -lt 1 ]; then
echo "Usage: $0 <filename>" >&2
exit 2
fi
echo "Processing: $1"
exit 0
Bash Strict Mode: set -e, set -o pipefail, and set -u Together
This section covers what no competitor article does: the full bash strict mode combination. Using set -e alone, which most tutorials teach, is not enough. Pipelines silently swallow exit codes unless you also add set -o pipefail.
The set -e Pipeline Trap (Critical Mistake):
With set -e alone, a failing command inside a pipeline does NOT trigger an exit. The pipeline exit code is the exit code of the last command. If the last command succeeds, the whole thing "succeeds" even if an earlier step failed silently.
Fix: Always combine set -e with set -o pipefail so the pipeline fails if ANY command in it fails.
LinuxTeck.com
set -e # Exit immediately on any command failure
set -o pipefail # Catch failures inside pipelines
set -u # Treat unset variables as errors
# Now this pipeline will FAIL if grep fails - without pipefail it would not
cat /var/log/app.log | grep "CRITICAL" | wc -l
Why set -u Matters:
Without set -u, a typo in a variable name silently expands to an empty string, which can cause commands like rm -rf $TYPO/ to expand to rm -rf /. Always use set -u in production scripts. You can combine all three on one line: set -euo pipefail. See our guide on bash variables and best practices for more defensive scripting patterns.
One-Line Strict Mode (Best Practice Template):
Start every production bash script with this single line at the top. It activates all three safeguards at once:
LinuxTeck.com
set -euo pipefail
Troubleshooting: Why set -e Breaks Inside if Statements:
A command that returns non-zero inside an if condition does NOT trigger set -e - bash intentionally disables it there because a non-zero result in a condition is expected and meaningful. This is a common source of confusion.
Fix: This is actually correct behaviour. Use if ! command; then or capture with command || true when you want to allow failure without exiting.
trap ERR: Build a Reusable Custom Error Handler in Bash
The trap command lets you register a function that runs automatically whenever a command fails or your script exits. Combined with a custom error_handler() function, this gives you line-by-line visibility into exactly where your script broke. No other Linux tutorial on this topic on this topic currently covers.
LinuxTeck.com
set -euo pipefail
# -- Reusable error handler ------------------------------
error_handler() {
local exit_code=$?
local line_number=$1
echo "" >&2
echo "--------------------------------------" >&2
echo " ERROR in script : ${BASH_SOURCE[0]}" >&2
echo " Failed at line : $line_number" >&2
echo " Exit code : $exit_code" >&2
echo "--------------------------------------" >&2
exit $exit_code
}
# Register the handler - runs on any ERR signal
trap 'error_handler $LINENO' ERR
# -- Cleanup handler -------------------------------------
cleanup() {
echo "Cleaning up temporary files..."
rm -rf /tmp/myapp_tmp_*
}
trap cleanup EXIT
# -- Your script logic starts here -----------------------
echo "Starting deployment..."
cp /nonexistent_config.yml /tmp/ # This will fail and trigger error_handler
echo "This line will not run."
cp: cannot stat '/nonexistent_config.yml': No such file or directory
--------------------------------------
ERROR in script : ./deploy.sh
Failed at line : 27
Exit code : 1
--------------------------------------
Cleaning up temporary files...
Copy-Paste Template:
The error_handler() + trap 'error_handler $LINENO' ERR + trap cleanup EXIT combination is a production-ready template. Drop it into any script. For long-running scripts, redirect the error output to a log file by appending 2>> /var/log/myscript.log to your echo lines. See our bash logging and stderr redirect guide for patterns.
Real-World Examples: Exit Codes in DevOps Scripts
Understanding exit code theory is only useful when you apply it to real scripts. Here are three production-style examples: a deployment script with rollback, a cron job with logging, and a dependency checker, all demonstrating how bash error handling works in practice.
Example 1 - Deployment Script with Automatic Rollback:
This pattern is used in CI/CD pipelines to roll back automatically if a deployment step fails. See our guide on automating Linux deployments with bash scripts for the full pipeline pattern.
LinuxTeck.com
set -euo pipefail
APP_DIR="/var/www/myapp"
BACKUP_DIR="/var/www/myapp_backup"
rollback() {
echo "[ROLLBACK] Deployment failed - restoring backup..." >&2
BACKUP_DIR="/var/www/myapp_backup"
echo "[ROLLBACK] Restore complete."
exit 1
}
trap rollback ERR
cp -r "$APP_DIR" "$BACKUP_DIR"
cd "$APP_DIR" && git pull origin main
npm install --production
systemctl restart myapp.service
echo "[SUCCESS] Deployment complete."
trap - ERR # Disable rollback trap after success
Example 2 - Cron Job with Exit Code Logging:
Cron gives you no output by default. Log exit codes explicitly so you have visibility when something silently fails at 3am. Pair this with our cron job tutorial for full scheduling examples.
LinuxTeck.com
LOG="/var/log/backup.log"
TIMESTAMP=$(date "+%Y-%m-%d %H:%M:%S")
log() { echo "[$TIMESTAMP] $1" >> "$LOG"; }
rsync -az /data/ /backup/data/ 2>>/var/log/backup_errors.log
EXIT_CODE=$?
if [ $EXIT_CODE -eq 0 ]; then
log "SUCCESS: Backup completed."
else
log "FAILURE: Backup failed with exit code $EXIT_CODE"
fi
exit $EXIT_CODE
Distro Notes: Exit Code Behaviour on Ubuntu, Rocky Linux, and CentOS
Bash exit codes and error handling work the same way across all major Linux distributions, but there are a few practical differences worth knowing when writing scripts for specific environments like Ubuntu servers or Rocky Linux enterprise systems.
Ubuntu 22.04 / 24.04 LTS:
Ubuntu ships with bash 5.x. The set -euo pipefail strict mode and all trap patterns in this article work fully. Note that Ubuntu's default shell for /bin/sh is dash, not bash - so always use #!/bin/bash explicitly in your shebang. Otherwise your scripts will run under dash, which does NOT support pipefail. Check your version with bash --version. See our Ubuntu bash scripting guide for more distro-specific patterns.
Rocky Linux 8 / 9 and RHEL:
Rocky Linux 8 ships with bash 4.4; Rocky Linux 9 ships with bash 5.1. All patterns in this article work on both. One practical difference: many enterprise scripts on RHEL-based systems use #!/bin/bash and rely on bash-specific features like PIPESTATUS, an array holding the exit codes of each command in the last pipeline. This is useful when pipefail is not set:
LinuxTeck.com
cat /var/log/app.log | grep "ERROR" | wc -l
echo "Exit codes: cat=${PIPESTATUS[0]}, grep=${PIPESTATUS[1]}, wc=${PIPESTATUS[2]}"
Exit codes: cat=0, grep=0, wc=0
CentOS 7 / End of Life Warning:
CentOS 7 reached end of life in June 2024. If you're still running scripts on CentOS 7 with bash 4.2, note that some newer bash features behave slightly differently. Migrate to Rocky Linux 8/9 or AlmaLinux for continued enterprise support. See our Rocky Linux migration guide for a step-by-step path.
Frequently Asked Questions - Bash Exit Codes & Error Handling
What does exit code 127 mean in bash?
Exit code 127 means command not found. Bash returns 127 when it cannot locate a command - either because it's not installed, not in your $PATH, or you have a typo. This is one of the most common errors in bash scripts that call external tools.
LinuxTeck.com
echo $?
127
Fix: Check your spelling, verify the tool is installed with which <command>, and confirm it's in your $PATH with echo $PATH. For scripts that depend on specific tools, add a dependency check at the start. See our guide to managing PATH in Linux for details.
How do I check the exit status of the last command in Linux?
Use the special variable $? immediately after the command. It holds the exit code of the last executed command: 0 for success, non-zero for failure. Always capture it right away if you need it later:
LinuxTeck.com
result=$? # Capture immediately
echo "Exit code was: $result"
Why does set -e not work inside an if statement?
This is intentional bash behaviour. When a command is used as the condition in an if, while, or until statement - or with !, &&, or ||, bash does NOT apply set -e to it. A non-zero return there is expected as part of the conditional logic, not treated as an error.
For example, if grep -q "pattern" file; then - if grep returns 1 (no match), that is not an error in this context. This is correct and by design. If you want to allow a command to fail without exiting, use command || true to always return 0.
What is the difference between set -e and set -o pipefail?
set -e exits the script if any simple command fails. However, it does NOT catch failures inside pipelines. The pipeline exit code is determined by the last command only. set -o pipefail changes this: it makes the pipeline return the exit code of the first failed command in the pipeline. You should always use both together:
LinuxTeck.com
What does exit code 126 mean and how do I fix it?
Exit code 126 means the file was found but could not be executed - typically a permission error. The script exists but does not have the executable bit set.
LinuxTeck.com
chmod +x myscript.sh
./myscript.sh
See our Linux file permissions guide (chmod explained) for full details on permission bits.
How do I log errors to a file in a bash script?
Redirect stderr (file descriptor 2) to a log file using 2>>. Use >> to append, not > which overwrites. To log both stdout and stderr to the same file, use >>logfile 2>&1.
LinuxTeck.com
your_command 2>> /var/log/script_errors.log
# Log both stdout and stderr
your_command >> /var/log/script.log 2>&1
What is PIPESTATUS in bash and when should I use it?
PIPESTATUS is a bash array holding the exit codes of each command in the most recently executed pipeline. Index 0 is the first command, 1 is the second, and so on. Use it when you need to check individual stages of a pipeline without enabling pipefail.
LinuxTeck.com
echo "cat: ${PIPESTATUS[0]}, grep: ${PIPESTATUS[1]}, sort: ${PIPESTATUS[2]}"
Summary
You now have at your complete a production ready collection of bash scripting tools for managing exit code errors; from reading $? after an application has exited, or exiting with an exit command, to turning bash into a strict shell by adding set -euo pipefail, to making a reusable trap ERR error handler for a real DevOps application.
These patterns in this guide are used daily in production Linux environments on Ubuntu and Rocky Linux; including the set -o pipefail pattern combination, and the custom error_handler() function.
To learn how to build even better automation systems, see our linux bash scripting automation series.
🔗 Related Articles
Learn step-by-step how to automate Linux tasks with real-world scripts and practical examples.