Linux Basic — Custom Error Functions
Custom Error Functions
Three or four small helper functions transform Bash scripts from “stack of commands” to “production tool.” This page shows the patterns that show up in every well-engineered shell script.
The Core Trio — die, warn, info
die() { printf 'ERROR: %s\n' "$*" >&2; exit 1; }
warn() { printf 'WARN: %s\n' "$*" >&2; }
info() { printf 'INFO: %s\n' "$*"; }
Three rules they encode:
- Errors and warnings go to stderr (
>&2) — so they’re visible even when stdout is piped dieexits non-zero — script aborts- Use
printfnotechofor predictable handling of%,-e, etc.
Use:
[[ -r "$config" ]] || die "Cannot read $config"
[[ -d "$out_dir" ]] || warn "$out_dir doesn't exist yet"
info "Processed $count items"
die With an Exit Code
die() {
local rc=1
if [[ "$1" =~ ^[0-9]+$ ]]; then
rc=$1; shift
fi
printf 'ERROR: %s\n' "$*" >&2
exit "$rc"
}
die 2 "Invalid arguments"
die "Generic failure"
Now die 2 "…" exits with code 2 (misuse convention); die "…" defaults to 1.
require_cmd — Validate Dependencies
require_cmd() {
for cmd in "$@"; do
command -v "$cmd" >/dev/null 2>&1 \
|| die "missing required command: $cmd"
done
}
require_cmd jq curl awk
Always check at the top of the script — fail fast, before any side effects.
Don’t use which — it’s not POSIX and has flaky exit codes on some systems. command -v is the right tool.
require_var — Validate Environment
require_var() {
for v in "$@"; do
[[ -n "${!v:-}" ]] || die "missing required env var: $v"
done
}
require_var API_TOKEN REGION ENV
${!v} is indirect expansion — v holds the name, ${!v} gives the value. The :- keeps set -u happy.
Or with the parameter-expansion shortcut:
: "${API_TOKEN:?API_TOKEN required}"
: "${REGION:?REGION required}"
The require_var function is friendlier when you want to validate several at once and print a consolidated list.
Log Levels — info, debug, warn, error
LOG_LEVEL=${LOG_LEVEL:-info}
_log() {
local level=$1; shift
local levels=(error warn info debug)
local current
for ((i=0; i<${#levels[@]}; i++)); do
[[ "${levels[$i]}" == "$LOG_LEVEL" ]] && current=$i
done
local this
for ((i=0; i<${#levels[@]}; i++)); do
[[ "${levels[$i]}" == "$level" ]] && this=$i
done
(( this <= current )) || return 0
printf '[%s] %-5s %s\n' "$(date +%H:%M:%S)" "${level^^}" "$*" >&2
}
error() { _log error "$@"; }
warn() { _log warn "$@"; }
info() { _log info "$@"; }
debug() { _log debug "$@"; }
Set LOG_LEVEL=debug ./myscript to see everything; default info shows the important bits.
A simpler binary form for “verbose mode”:
verbose=${VERBOSE:-0}
debug() { (( verbose )) || return 0; printf 'DEBUG: %s\n' "$*" >&2; }
Colorized Output
Make errors visually obvious — but only when stderr is a real terminal:
if [[ -t 2 ]]; then
RED=$'\e[31m'
YELLOW=$'\e[33m'
BLUE=$'\e[34m'
BOLD=$'\e[1m'
RESET=$'\e[0m'
else
RED='' YELLOW='' BLUE='' BOLD='' RESET=''
fi
die() { printf '%sERROR:%s %s\n' "$RED$BOLD" "$RESET" "$*" >&2; exit 1; }
warn() { printf '%sWARN:%s %s\n' "$YELLOW$BOLD" "$RESET" "$*" >&2; }
info() { printf '%sINFO:%s %s\n' "$BLUE" "$RESET" "$*"; }
[[ -t 2 ]] returns true if file descriptor 2 (stderr) is connected to a terminal. When redirected to a file or piped, colors are disabled — no escape-code noise in your log files.
NO_COLOR is becoming a standard environment variable; honour it:
if [[ -z "${NO_COLOR:-}" && -t 2 ]]; then
RED=$'\e[31m'
…
fi
Error Context — die_at
Sometimes you want the error message to include where it came from:
die_at() {
local rc=1
if [[ "$1" =~ ^[0-9]+$ ]]; then rc=$1; shift; fi
printf 'ERROR at %s:%d: %s\n' "${BASH_SOURCE[1]}" "${BASH_LINENO[0]}" "$*" >&2
exit "$rc"
}
# Use:
[[ -r config.txt ]] || die_at "config.txt unreadable"
# ERROR at /path/to/script.sh:42: config.txt unreadable
BASH_SOURCE[1] and BASH_LINENO[0] give you the caller’s file and line.
Putting It All Together — A Library File
Save these helpers in lib.sh:
# lib.sh — common helpers
# Source with: . "$(dirname "$0")/lib.sh"
die() { printf 'ERROR: %s\n' "$*" >&2; exit "${EXIT_CODE:-1}"; }
warn() { printf 'WARN: %s\n' "$*" >&2; }
info() { printf 'INFO: %s\n' "$*"; }
require_cmd() {
local cmd
for cmd in "$@"; do
command -v "$cmd" >/dev/null || die "missing command: $cmd"
done
}
require_var() {
local v
for v in "$@"; do
[[ -n "${!v:-}" ]] || die "missing env var: $v"
done
}
run() {
info "+ $*"
"$@"
}
In every script:
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
. "$(dirname "$0")/lib.sh"
require_cmd jq curl
require_var API_TOKEN
main() {
info "Starting"
run curl -sS --fail "$URL" | jq .data
info "Done"
}
main "$@"
That four-line header (strict mode + library + dep checks) makes any Bash script look professional and behave predictably.
Common Mistakes
| ❌ Wrong | ✅ Right | Issue |
|---|---|---|
echo "ERROR…" (to stdout) |
echo "ERROR…" >&2 (or printf) |
errors get piped along with data |
echo "%s value" accidentally |
printf '%s\n' "$msg" |
echo has weird behaviour with -e/-n |
which jq for “is jq installed?” |
command -v jq |
which isn’t POSIX |
Setting LOG_LEVEL but ignoring it |
actually check it before printing | unused configs are confusing |
| Coloring output that’s being piped to a log file | check [[ -t 2 ]] first |
escape codes pollute logs |
Next → Practice Exercises