Bash Shell
Scripting
Automation
Error Handling
Programming

Automatic exit from Bash shell script on error

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

The standard way to make a Bash script exit automatically on error is set -e. In production scripts, the stronger pattern is set -euo pipefail, which also catches unset variables and hidden pipeline failures. This article covers all three flags, the important exceptions to set -e behavior, explicit error handling patterns, and a production-ready script template you can use as a starting point.

The Basic Tool: set -e

set -e (also known as errexit) tells Bash to exit immediately when a simple command returns a non-zero exit status.

bash
1#!/usr/bin/env bash
2set -e
3
4echo "Step 1: Creating directory"
5mkdir /tmp/myapp
6echo "Step 2: Copying files"
7cp nonexistent.txt /tmp/myapp/
8echo "Step 3: This line never runs"

When cp fails because the source file does not exist, the script exits with the same non-zero status. Without set -e, the script would silently continue to Step 3, potentially operating on incomplete data.

Why set -euo pipefail Is the Industry Standard

Most production Bash scripts use all three flags together:

bash
#!/usr/bin/env bash
set -euo pipefail

Each flag addresses a different class of silent failure:

FlagNameWhat It Catches
-eerrexitCommands that return non-zero exit status
-unounsetReferences to undefined variables
-o pipefailpipefailFailures in any command within a pipeline

Why -u Matters

Without -u, typos in variable names silently expand to empty strings:

bash
1#!/usr/bin/env bash
2set -e
3# Note: no -u flag
4
5rm -rf "$OUTPTU_DIR/build"  # Typo: OUTPTU_DIR instead of OUTPUT_DIR
6# Expands to: rm -rf "/build" if variable is empty

With -u, the script exits immediately with an error message identifying the unset variable.

Why pipefail Matters

Without pipefail, a pipeline's exit status is the exit status of the last command only:

bash
1#!/usr/bin/env bash
2set -e
3# Note: no pipefail
4
5cat nonexistent_file.txt | sort | head -5
6echo "This line runs even though cat failed"

The cat failure is masked because head succeeds. With pipefail, the pipeline inherits the non-zero status from cat, and set -e terminates the script.

bash
1#!/usr/bin/env bash
2set -euo pipefail
3
4cat nonexistent_file.txt | sort | head -5
5echo "This line never runs"

Important Exceptions to set -e

set -e does not trigger on every non-zero exit status. Understanding the exceptions prevents confusion:

Commands in Conditional Context

Commands used as conditions in if, while, or until statements are allowed to fail:

bash
1#!/usr/bin/env bash
2set -e
3
4# grep returning 1 (no match) does NOT exit the script
5if grep -q "pattern" file.txt; then
6    echo "Found"
7else
8    echo "Not found"
9fi
10
11echo "Script continues normally"

Commands Before Logical Operators

Commands on the left side of && or || are exempt:

bash
1#!/usr/bin/env bash
2set -e
3
4false && echo "This does not print"
5echo "Script continues because false was before &&"

Commands in Subshells and Command Substitution

The behavior in subshells depends on the Bash version and context. In general, set -e does not propagate into command substitutions in all cases:

bash
1#!/usr/bin/env bash
2set -e
3
4# This may NOT exit the script depending on Bash version
5result=$(false; echo "still running")

Complete Exception Reference

ContextDoes set -e Trigger?Example
Simple commandYesfalse exits the script
if conditionNoif false; then ... continues
Left side of &&Nofalse && echo x continues
Left side of ||Nofalse || echo x continues
Command substitutionVaries by Bash version$(false) behavior is inconsistent
Functions called in conditionalNoif my_func; then ... continues

Handling Expected Failures Explicitly

When a command might legitimately fail, suppress it explicitly rather than disabling strict mode:

bash
1#!/usr/bin/env bash
2set -euo pipefail
3
4# Pattern 1: || true for commands that may fail harmlessly
5rm -f old-output.txt 2>/dev/null || true
6
7# Pattern 2: Conditional check when you need the result
8if command -v docker &>/dev/null; then
9    echo "Docker is available"
10else
11    echo "Docker not found, skipping container tests"
12fi
13
14# Pattern 3: Capture exit code for branching
15set +e
16pg_isready -h localhost -p 5432
17db_status=$?
18set -e
19
20if [ "$db_status" -ne 0 ]; then
21    echo "Database not ready, starting it..."
22    pg_ctl start -D /var/lib/postgresql/data
23fi

The || true pattern is the most common. It makes the intent clear: this specific command is allowed to fail. Use it sparingly and only when the failure is genuinely harmless.

Add a Trap for Better Debugging

An ERR trap runs a command whenever a non-zero exit status triggers set -e. This is invaluable for diagnosing failures in CI pipelines and automated deployments:

bash
1#!/usr/bin/env bash
2set -euo pipefail
3
4cleanup() {
5    local exit_code=$?
6    echo "ERROR: Script failed on line $1 with exit code $exit_code" >&2
7    # Add cleanup logic here: remove temp files, release locks, etc.
8    rm -f /tmp/myapp.lock
9}
10
11trap 'cleanup $LINENO' ERR

For scripts that need cleanup regardless of success or failure, trap EXIT instead:

bash
1#!/usr/bin/env bash
2set -euo pipefail
3
4TMPDIR=$(mktemp -d)
5
6cleanup() {
7    rm -rf "$TMPDIR"
8}
9trap cleanup EXIT
10
11# Script body uses $TMPDIR safely
12cp important.dat "$TMPDIR/working.dat"
13process "$TMPDIR/working.dat"

The EXIT trap fires on normal exit, error exit, and most signals, making it the right choice for resource cleanup.

A Production-Ready Script Template

This template combines all the patterns discussed into a reusable starting point:

bash
1#!/usr/bin/env bash
2set -euo pipefail
3
4# Trap for error diagnostics
5trap 'echo "FAILED: line $LINENO, exit code $?" >&2' ERR
6
7# Constants
8readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
9readonly LOG_FILE="/tmp/$(basename "$0").log"
10
11log() {
12    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
13}
14
15main() {
16    log "Starting deployment"
17
18    # Validate prerequisites
19    command -v docker &>/dev/null || { log "docker not found"; exit 1; }
20    command -v kubectl &>/dev/null || { log "kubectl not found"; exit 1; }
21
22    # Expected-failure example
23    docker stop old-container 2>/dev/null || true
24
25    # Main work
26    log "Building image"
27    docker build -t myapp:latest "$SCRIPT_DIR"
28
29    log "Deploying"
30    kubectl apply -f "$SCRIPT_DIR/k8s/"
31
32    log "Deployment complete"
33}
34
35main "$@"

This gives you automatic failure on unexpected errors, safe variable handling, pipeline failure detection, diagnostic output with line numbers, and a clean structure that separates concerns.

Common Pitfalls

  • Assuming set -e is universal. It has documented exceptions for conditionals, logical operators, and subshells. Read the Bash manual section on errexit to understand the edge cases.
  • Using set -e without pipefail. A failing command early in a pipeline is silently masked by a successful command at the end. Always pair them.
  • Overusing || true. Every || true is a place where errors are intentionally ignored. If you find yourself adding it to many lines, the script may need restructuring rather than more exception suppression.
  • Forgetting -u and getting silent empty-string expansion. Variable typos are one of the most common causes of destructive Bash bugs, like rm -rf $UNDEFINED/ expanding to rm -rf /.
  • Using set -e in library functions sourced by other scripts. If the calling script does not use set -e, the behavior is inconsistent. Library functions should validate their own preconditions explicitly.
  • Not testing trap behavior. ERR traps do not fire in the same exception contexts where set -e does not fire. Test your error handling with deliberate failures during development.

Summary

  • set -e exits the script on command failure, but has important exceptions for conditionals and logical operators.
  • set -euo pipefail is the standard strict mode for production scripts, catching errors, unset variables, and pipeline failures.
  • Handle expected failures explicitly with || true, conditional checks, or temporary set +e blocks.
  • Use trap ... ERR for diagnostic output and trap ... EXIT for cleanup that must run regardless of outcome.
  • Structure scripts with a main function, readonly constants, and clear prerequisite validation.
  • Test error handling deliberately. Do not assume strict mode catches everything without verifying the behavior in your specific Bash version.

Course illustration
Course illustration

All Rights Reserved.