Marcio Cunha

How to write robust Bash scripts with error handling

Learn how to develop secure Linux automation routines in Bash by implementing robust error handling, structured logs, and rigorous parameter validation.

Marcio Cunha•5 min
Also available in:PortuguêsEspañol
Summary
  • Secure automation scripts require early parameter validation to prevent silent failures
  • Enforcing strict mode prevents commands from failing without stopping execution flow
  • Recording structured logs with timestamps simplifies audits and system diagnostics
  • Encapsulated functions keep code modular and significantly easier to maintain daily
  • Automating routines without redundancy and checks exposes systems to severe operational risks

Why Bash scripts fail in production environments

Creating automation routines in the terminal is one of the most common tasks for system administrators and developers. However, a script written quickly to solve an immediate problem often turns into a headache when executed on production servers. In practice, this means a silently failed command can corrupt data or take down entire services without anyone noticing in time. To prevent this type of disaster, software engineering applied to shell scripts demands much more than just a sequence of chained commands.

When we write automations, we assume an ideal scenario where files exist, disks have free space, and access permissions are correct. The real world, on the other hand, is full of unpleasant surprises. If a backup directory cannot be created, a fragile script will keep deleting old files or trying to send data into nowhere. Building resilient routines involves anticipating the anomalous behavior of every external dependency and ensuring the program stops immediately upon detecting any operational inconsistency, much like what we explore in how to automate server backups using Bash scripts and rsync.

How to enable strict mode to protect your script

The first step to elevate the code quality of any Bash script is using error-control directives on the very first line of the file. By default, the Linux interpreter executes the next line even if the previous one returns a critical error. This usually generates a catastrophic domino effect. By adding strict configuration commands right after the shebang, you fundamentally alter the security behavior of the script.

In practice, we use the combination of set options to impose strict rules on the interpreter. The option that stops execution immediately upon encountering a failed command is indispensable, as is the directive that treats undeclared variables as fatal errors. Additionally, complex pipelines need to propagate the error code of the command that actually failed, rather than just the last one in the chain. Applying these settings turns a naive script into a much more predictable and safe tool for the operational environment.

#!/usr/bin/env bash
set -euo pipefail
IFS=$'
	'

echo "Starting execution with strict mode enabled..."

How to structure input parameter validation

Every useful script needs to receive external data, whether through command-line arguments, environment variables, or configuration files. Blindly trusting that the user will enter data in the correct order is an invitation to bizarre failures. Parameter validation consists of exhaustively checking if necessary inputs were provided before initiating any destructive operation on the system.

To implement this check cleanly, we verify the number of expected arguments using native shell variables. If the number of inputs is insufficient, we display a clear usage message and exit with a non-zero exit code. Validating data types, directory paths, and the existence of essential binaries — such as curl or awk — prevents the script from breaking midway due to missing basic dependencies.

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

CONFIG_FILE="${1:-}"

if [ -z "${CONFIG_FILE}" ]; then
    echo "Error: No configuration file was provided." >&2
    echo "Usage: $0 /path/to/config.conf" >&2
    exit 1
fi

if [ ! -f "${CONFIG_FILE}" ]; then
    echo "Error: The file '${CONFIG_FILE}' does not exist." >&2
    exit 1
fi

How to implement structured and traceable logs

When something goes wrong on a remote server in the middle of the night, the operator does not want to guess what happened. They need clear, organized logs with precise timestamps. Printing loose messages to the screen using a standard echo command is not enough when output needs to be redirected to audit files or centralized monitoring systems.

A professional logging system uses file descriptors to separate standard error output from informative messages. Creating a dedicated function to record events ensures that all lines follow a standardized format, containing the date, severity level, and descriptive message. This immensely facilitates integration with analysis tools and accelerates incident resolution in complex corporate environments.

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

log() {
    local level="${1}"
    shift
    local message="*"
    local timestamp
    timestamp=$(date '+%Y-%m-%d %H:%M:%S')
    echo "[${timestamp}] [${level}] ${message}"
}

log "INFO" "Initialization process completed successfully."
log "ERROR" "Failed to connect to the remote database." >&2

How to manage traps and resource cleanup

During the execution of a robust script, temporary files are created, network connections are opened, and execution locks are activated to prevent simultaneous instances. If the script is abruptly interrupted by a system signal or an unexpected error, these orphan resources continue consuming space and can block future executions.

To solve this problem elegantly, we use the shell trap command. It allows us to program routines that will execute obligatorily the moment the script terminates, whether due to success, error, or manual interruption by the operator. This way, we ensure that temporary files are deleted and execution locks are released cleanly, keeping the operating system organized and free of digital clutter. To complement this implementation in production environments, see our related technical analyses on How to inspect PWM signals on boards with an oscilloscope and How to calculate voltage drop in long electrical cables.

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

TEMP_DIR=$(mktemp -d)

cleanup() {
    echo "Cleaning up temporary resources in ${TEMP_DIR}..."
    rm -rf "${TEMP_DIR}"
}

trap cleanup EXIT

echo "Working with temporary files..."
sleep 2

echo "Process completed successfully."

Final thoughts on secure Linux automation

Developing Bash scripts goes far beyond stringing together commands that work in an interactive terminal. The transition from an amateur script to an engineering routine requires discipline in input validation, rigor in error handling, and predictability in failure behavior. Investing time structuring code saves precious hours of future debugging on production servers. Adopting good shell development practices ensures your automations are reliable, auditable, and ready to run for years without unwanted manual intervention.