Linux / Bash

How to Fix Bash Script "set -euo pipefail" Breaking on Empty Variable Expansion

4 min read by DebuggedIt

Quick answer

You add set -euo pipefail to a Bash script β€” good practice for catching errors early β€” and suddenly the script fails on a line that worked fine before,...

You add set -euo pipefail to a Bash script β€” good practice for catching errors early β€” and suddenly the script fails on a line that worked fine before, specifically involving a variable that's empty or unset. This is set -u (part of the combined flag set) doing exactly what it's designed to do: treating any reference to an unset variable as a hard error instead of silently substituting an empty string.

The Problem

A script that ran without issue before now fails immediately on a specific line:

#!/bin/bash
set -euo pipefail

echo "Deploying to: $ENVIRONMENT"
$ ./deploy.sh
./deploy.sh: line 4: ENVIRONMENT: unbound variable

This is especially common right after adding strict mode to an existing script that previously relied on optional environment variables silently defaulting to empty.

Why It Happens

set -u (or the equivalent set -o nounset) makes Bash treat any reference to an unset variable as an error and exit immediately, rather than the default Bash behavior of silently expanding an unset variable to an empty string. This is specifically the point of enabling it β€” catching typos in variable names and genuinely missing required inputs before they cause subtle, hard-to-debug downstream behavior β€” but it means any script relying on the old "unset variables are just empty" behavior needs to be adjusted once this flag is added. Common places this surfaces:

  • Optional environment variables that a script expects might not always be set, previously handled implicitly by them just evaluating to empty strings.
  • Positional parameters ($1, $2, etc.) that aren't guaranteed to be provided by every caller of the script.
  • Variables set conditionally in an earlier branch that isn't always executed, leaving the variable unset on some code paths but referenced unconditionally later.
  • Arrays that are empty β€” referencing an unset or empty array in certain contexts can also trigger this under set -u, depending on the specific Bash version and how the array is referenced.

The Fix

For variables that are legitimately optional, use Bash's default-value parameter expansion instead of a bare reference, which provides a fallback value without erroring under set -u:

echo "Deploying to: ${ENVIRONMENT:-staging}"

This uses staging if ENVIRONMENT is unset or empty, without triggering the unbound variable error. If you want the fallback to only apply when the variable is genuinely unset (not when it's explicitly set to an empty string), use the single-colon-less variant:

echo "Deploying to: ${ENVIRONMENT-staging}"

The distinction: :- triggers the default for both "unset" and "set but empty," while a bare - (no colon) triggers the default only for "genuinely unset." Choose based on whether an explicitly empty value should be treated the same as no value at all for your specific use case.

For values that are genuinely required and should cause an immediate, clear failure if missing, use the error-message variant instead of just silently substituting a default β€” this fails fast with an intentional, readable message rather than relying on Bash's own generic "unbound variable" error:

: "${REQUIRED_API_KEY:?Error: REQUIRED_API_KEY must be set}"

This produces a clear, custom error message if the variable is missing, while doing nothing at all (the leading : is a no-op command) if it's present β€” a clean, idiomatic pattern for validating required inputs early in a script.

For positional parameters that might not always be provided, apply the same default-value pattern:

ENV="${1:-production}"
echo "Deploying to: $ENV"

For arrays, check length explicitly before iterating if the array might legitimately be empty, since set -u combined with certain array reference patterns (especially on older Bash versions) can behave inconsistently:

if [ ${#items[@]} -gt 0 ]; then
    for item in "${items[@]}"; do
        echo "$item"
    done
fi

Still Not Working?

If you're not sure exactly which variable reference is triggering the failure in a longer script, run with set -x added temporarily alongside your existing strict-mode flags, which prints every command as it executes right before it runs β€” the last line printed before the failure message tells you exactly which command and variable reference caused it:

#!/bin/bash
set -euxo pipefail  # added x for command tracing
$ ./deploy.sh
+ echo 'Deploying to: '
./deploy.sh: line 4: ENVIRONMENT: unbound variable

Remove the x flag again once you've identified and fixed the specific issue, since the verbose command tracing is primarily a debugging aid rather than something you'd typically want in normal production script output.