Bash Error Guide: 'readonly variable' — Fix Assignments to Locked Vars
Fix Bash 'readonly variable': stop reassigning a variable declared readonly, rename colliding names, and avoid re-sourcing files that redeclare constants.
- #bash
- #automation
- #troubleshooting
- #errors
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
Bash prints readonly variable when a script tries to assign to a variable that was declared readonly (or declare -r):
./deploy.sh: line 22: VERSION: readonly variable
A readonly variable is a deliberate constant — once set, Bash refuses any later assignment and, under set -e, may abort the script. The error means code somewhere tried to change a value that was locked. Common triggers are re-sourcing a file that declares constants twice, a name collision with a shell-reserved or already-frozen variable, or an honest attempt to mutate a value meant to be fixed.
Symptoms
- An assignment to a specific name fails while every other assignment works.
- The failure appears the second time a config file is sourced, not the first.
- Reusing a common name (
SECONDS,UID,BASHPID,PPID) as your own variable fails. - Under
set -euo pipefailthe script exits at the assignment line with this message.
Common Root Causes
- Re-sourcing a constants file —
source config.shruns twice (e.g. once from a parent, once from a child), and itsreadonly VERSION=...runs again on the now-frozen name. - Assigning to a shell-maintained readonly — names like
UID,EUID,PPID,BASHPIDare readonly by the shell. - A name collision — your variable shares a name with one another sourced file already froze.
declare -r/readonlyearlier in the same script, then later code tries to update it.- Exported constants inherited from the environment as readonly (rare, but possible via a parent shell).
Diagnostic Workflow
List which variables are currently readonly:
readonly -p | grep -i version # is VERSION frozen, and to what value?
Find where the name is declared readonly:
grep -rn 'readonly\|declare -r' . | grep VERSION
Trace to see the double-source or the offending reassignment:
bash -x ./deploy.sh 2>&1 | grep -n 'VERSION='
Confirm whether a file is being sourced more than once by adding a guard probe:
echo "sourcing config from ${BASH_SOURCE[1]:-?}" >&2
Example Root Cause Analysis
A deploy pipeline failed intermittently:
deploy.sh: line 22: RELEASE_ID: readonly variable
The constants lived in env.sh:
# env.sh
readonly RELEASE_ID="$(date +%Y%m%d%H%M)"
deploy.sh sourced env.sh, then called helpers.sh, which also sourced env.sh. The second source re-ran readonly RELEASE_ID=... on the already-frozen name, and set -e aborted. The fix is an include guard so the file’s body runs at most once:
# env.sh
[[ -n "${__ENV_SH_LOADED:-}" ]] && return
__ENV_SH_LOADED=1
readonly RELEASE_ID="${RELEASE_ID:-$(date +%Y%m%d%H%M)}"
The return short-circuits repeat sources, and defaulting with ${RELEASE_ID:-...} makes re-declaration idempotent even if the guard is bypassed.
Prevention Best Practices
- Add include guards to sourced files — a
__LOADEDsentinel withreturnprevents double-declaration of constants. - Make readonly declarations idempotent —
readonly X="${X:-default}"so a second run doesn’t fight the first. - Avoid shell-reserved names — don’t use
UID,PPID,SECONDS,BASHPID, etc. for your own variables. - Reserve
readonlyfor true constants — don’t freeze values you later need to update. - Namespace your variables — prefix project constants (
APP_VERSION) to dodge collisions with other sourced files. - Run ShellCheck, which flags assignments to known-readonly and reserved variables.
Quick Command Reference
readonly -p # list all readonly variables and values
readonly -p | grep NAME # check if a specific name is frozen
grep -rn 'readonly\|declare -r' . # find where constants are declared
# idempotent constant:
readonly APP_VERSION="${APP_VERSION:-1.0.0}"
# include guard at top of a sourced file:
[[ -n "${__LIB_LOADED:-}" ]] && return; __LIB_LOADED=1
Related Guides
- Bash Error Guide: ‘unbound variable’ — the opposite failure under
set -u: reading a variable that was never set. - Bash Error Guide: ‘bad substitution’ — other variable-expansion failures at parse time.
- Bash & Python Error Guide: ‘command not found’ — sourcing and PATH issues that surface when splitting scripts into libs.
Conclusion
readonly variable is Bash protecting a constant from being reassigned. The usual cause isn’t a logic bug but a file sourced twice re-running its readonly declarations, or a collision with a shell-reserved name. Add include guards to sourced files, make constant declarations idempotent with ${X:-default}, and namespace your variables. Then constants stay constant without aborting the script on the second load.
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?
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.