Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for Bash & Python Automation By James Joyner IV · · 9 min read Last reviewed Jul 2026

Bash Error Guide: 'unbound variable' — Fix set -u Failures Safely

Quick answer

Fix Bash 'unbound variable' errors under set -u: safely default unset variables, guard optional parameters and arrays, and stop scripts exiting on a missing or empty value.

  • #bash
  • #automation
  • #troubleshooting
  • #errors
Free toolkit

Stuck on this Bash & Python Automation error? Get the free incident triage checklist

A one-page PDF — the exact steps to isolate, fix, and verify a production error like this one. No spam, unsubscribe anytime.

Overview

You enabled strict mode (set -u or set -Eeuo pipefail) to catch typos and missing inputs early, and now a script that “used to work” aborts the moment it references a variable that was never set. Bash prints the offending name and exits non-zero:

./deploy.sh: line 42: DATABASE_URL: unbound variable

Under set -u (also written set -o nounset), referencing any variable that has no value is a hard error, not an empty string. This is almost always what you want in automation — it turns silent bugs like rm -rf "$PREFI/" into a loud failure — but it also fires on legitimately optional parameters, unset environment variables, and empty positional arguments unless you write the expansions defensively.

Symptoms

  • The script exits immediately with a non-zero status (typically 1) the first time an unset variable is expanded.
  • The message names the variable and line: line 42: DATABASE_URL: unbound variable.
  • It only started after adding set -u / set -euo pipefail; the same code ran “fine” before because the variable silently expanded to an empty string.
  • Positional parameters trigger it when an argument is omitted: $1: unbound variable.
  • Array expansions trigger it on empty arrays under older Bash: line 12: array[@]: unbound variable.
  • In CI it may pass locally (where the env var is exported) and fail on the runner (where it isn’t).

Common Root Causes

  • A genuinely missing environment variable — the job or .env never exported DATABASE_URL, so the fix is to provide it, not to suppress the error.
  • An optional flag or parameter referenced directly ($OPTIONAL_TAG) instead of with a default.
  • A typo$DATABSE_URL vs $DATABASE_URL; set -u is doing its job by catching it.
  • Positional parameters used without checking $# first (dest="$2" when only one arg was passed).
  • Empty arrays expanded as "${arr[@]}" on Bash before 4.4, which treated an empty array as unset.
  • Sourced/partial config — a config file that was supposed to define the variable failed to load or was conditional.
  • $@/$* in functions with no arguments on old Bash versions.

Diagnostic Workflow

First, confirm strict mode is on and reproduce the exact line:

grep -nE 'set -[Eeuo]+|set -o nounset' deploy.sh
bash -u ./deploy.sh 2>&1 | tail -5

Find every place the named variable is used so you know which reference is unguarded:

grep -n 'DATABASE_URL' deploy.sh

Check whether the variable is actually set in the current environment (distinguish “unset” from “set but empty”):

# prints "unset" if never set, "empty" if set to "", else the value
if [ -z "${DATABASE_URL+x}" ]; then echo unset; \
elif [ -z "$DATABASE_URL" ]; then echo empty; \
else echo "set: $DATABASE_URL"; fi

Reproduce the class of failure in isolation to understand the trigger:

$ bash -c 'set -u; echo "$MISSING"'
bash: line 1: MISSING: unbound variable

$ bash -c 'set -u; echo "${MISSING:-default}"'
default

Trace execution to see the exact expansion that fails, with line numbers:

bash -xu ./deploy.sh 2>&1 | grep -n 'unbound\|DATABASE_URL'

Example Root Cause Analysis

A deploy script fails on a CI runner but works on the author’s laptop:

+ TAG=main
+ REGISTRY=registry.example.com
./deploy.sh: line 42: IMAGE_TAG_SUFFIX: unbound variable

Line 42 was:

image="$REGISTRY/app:${TAG}${IMAGE_TAG_SUFFIX}"

IMAGE_TAG_SUFFIX is an optional variable — set to something like -rc only for release candidates, and left unset for normal builds. On the author’s laptop it happened to be exported in their shell profile, so the script never hit the error. On the clean CI runner it was unset, and set -u correctly aborted.

The wrong fix is to remove set -u (that would re-hide real typos elsewhere). The right fix is to declare the intent — this variable is optional and defaults to empty:

image="$REGISTRY/app:${TAG}${IMAGE_TAG_SUFFIX:-}"

For a variable that is required, the fix is the opposite — fail with a clear message instead of a cryptic one:

: "${DATABASE_URL:?DATABASE_URL must be set (export it or add it to .env)}"

This prints DATABASE_URL: DATABASE_URL must be set ... and exits, which is far more actionable than the default message.

Prevention Best Practices

  • Keep set -u. The error is a feature. Fix the expansions, don’t disable nounset.
  • Default optional variables explicitly with ${VAR:-default} (default if unset or empty) or ${VAR-default} (default only if unset). Know the difference.
  • Assert required variables early at the top of the script with : "${VAR:?message}" so failures are named and grouped, not discovered deep in the logic.
  • Guard positionals — check [ "$#" -ge 2 ] or use "${1:-}" before referencing arguments that may be absent.
  • For arrays, expand as "${arr[@]:-}" or bump to Bash 4.4+ where empty arrays no longer trip set -u; test with ${#arr[@]} before iterating.
  • Validate config loading — after sourcing a config file, assert the keys you expected actually got set.
  • Lint with ShellCheck (SC2154 flags variables that appear unset) as part of CI.

Quick Command Reference

# Default value if unset OR empty
echo "${VAR:-fallback}"

# Default value only if unset (empty stays empty)
echo "${VAR-fallback}"

# Require the variable, fail with a message if unset/empty
: "${VAR:?must be set}"

# Test set-vs-empty without triggering set -u
[ -n "${VAR+x}" ] && echo "is set (maybe empty)"

# Safe optional array expansion
for x in "${items[@]:-}"; do echo "$x"; done

# Trace which line trips nounset
bash -xu ./script.sh

Conclusion

unbound variable is set -u doing exactly what you asked: refusing to silently paper over a missing value. Treat it as a signal, not a nuisance. For truly optional inputs, add an explicit default with ${VAR:-}; for required ones, assert them up front with ${VAR:?message} so the failure names itself. Keep strict mode on, lint with ShellCheck, and validate that config actually loaded — and these errors turn from mysterious mid-run aborts into clear, early, actionable messages.

Free download · 368-page PDF

Fixed it? Get 500 Bash & Python Automation & DevOps AI prompts — free

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.

Did this fix your issue?

Free download · 368-page PDF

Get 500 Battle-Tested DevOps AI Prompts — Free

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.