nats-io/nats-server · error

variable reference cycle for '%s'

Error message

variable reference cycle for '%s'

What it means

After exhausting all context maps without finding the variable, lookupVariable detects a self-referential cycle: p.envVarReferences already contains varReference, meaning this same reference is being resolved recursively (a variable whose resolution re-triggers itself). The parser aborts rather than looping forever.

Source

Thrown at conf/parse.go:468

	if strings.HasPrefix(varReference, bcryptPrefix) {
		return "$" + varReference, true, nil
	}

	// Loop through contexts currently on the stack.
	for i := len(p.ctxs) - 1; i >= 0; i-- {
		ctx := p.ctxs[i]
		// Process if it is a map context
		if m, ok := ctx.(map[string]any); ok {
			if v, ok := m[varReference]; ok {
				return v, ok, nil
			}
		}
	}

	// If we are here, we have exhausted our context maps and still not found anything.
	// Detect reference cycles
	if p.envVarReferences[varReference] {
		return nil, false, fmt.Errorf("variable reference cycle for '%s'", varReference)
	}
	p.envVarReferences[varReference] = true
	defer delete(p.envVarReferences, varReference)

	// Parse from the environment
	if vStr, ok := os.LookupEnv(varReference); ok {
		// Everything we get here will be a string value, so we need to process as a parser would.
		if subp, err := parseEnv(fmt.Sprintf("%s=%s", pkey, vStr), p); err == nil {
			v, ok := subp.mapping[pkey]
			return v, ok, nil
		} else {
			return nil, false, err
		}
	}
	return nil, false, nil
}

func (p *parser) setValue(val any) {

View on GitHub (pinned to 3a66a489d2)

Solutions

  1. Break the cycle: make the variable's value literal instead of a reference to itself.
  2. Rename one of the mutually-referencing variables so the chain terminates at a literal.
  3. Set the terminal value directly in the environment or config instead of through the recursive reference.
  4. Trace the reference chain named in the error to find which entry loops back.

Example fix

// before
HOME_DIR = ${HOME_DIR}/bin
// after
HOME_DIR = /home/user
BIN_DIR = ${HOME_DIR}/bin
Defensive patterns

Strategy: validation

Validate before calling

// Detect self-references before parsing
for key, val := range cfg {
	if s, ok := val.(string); ok && strings.Contains(s, "${"+key+"}") {
		return fmt.Errorf("variable %s references itself", key)
	}
}

Try / catch

if err := parseConfig(); err != nil {
	if strings.Contains(err.Error(), "variable reference cycle") {
		return fmt.Errorf("cyclic variable definition in config: %w", err)
	}
	return err
}

Prevention

When it happens

Trigger: lookupVariable (called by processItem) is re-entered for the same varReference while p.envVarReferences[varReference] is still true — e.g. a variable value that itself references the same variable, or include chains that re-enter the same reference.

Common situations: A config key defined as a reference to itself (${PATH} = ${PATH} in config), two variables referencing each other, or an env var plus config entry pointing back at the same reference so resolution never terminates.

Related errors


AI-assisted analysis of nats-io/nats-server@3a66a489d2 (2026-09-02). Data as JSON: /api/errors/0b6da333a53bd175. Report an issue: GitHub.