hashicorp/terraform · warning

state not locked

Error message

state not locked

What it means

Returned by lockMap.unlock (internal/backend/remote-state/inmem/backend.go:205) when there is no entry in the global locks map for the given state name. Calling Unlock on a state that was never locked (or was already unlocked) is treated as an error rather than a silent no-op.

Source

Thrown at internal/backend/remote-state/inmem/backend.go:205

		// make a copy of the lock info to avoid any testing shenanigans
		*lockErr.Info = *lockInfo
		return "", lockErr
	}

	info.Created = time.Now().UTC()
	l.m[name] = info

	return info.ID, nil
}

func (l *lockMap) unlock(name, id string) error {
	l.Lock()
	defer l.Unlock()

	lockInfo := l.m[name]

	if lockInfo == nil {
		return errors.New("state not locked")
	}

	lockErr := &statemgr.LockError{
		Info: &statemgr.LockInfo{},
	}

	if id != lockInfo.ID {
		lockErr.Err = errors.New("invalid lock id")
		*lockErr.Info = *lockInfo
		return lockErr
	}

	delete(l.m, name)
	return nil
}

View on GitHub (pinned to c9def3e214)

Solutions

  1. Only call Unlock if Lock returned a non-empty lock ID and no error.
  2. Guard double-unlock by clearing the stored lock ID after a successful unlock.
  3. Call inmem.Reset() at the start of each test, not mid-operation.

Example fix

// before: unconditional unlock
sm.Unlock(lockID)

// after: only unlock when actually locked
var lockID string
if lockID, err := sm.Lock(info); err == nil {
    defer sm.Unlock(lockID)
}
Defensive patterns

Strategy: validation

Validate before calling

var lockID string
if id, err := sm.Lock(info); err == nil {
    lockID = id
}
// later, only unlock if we actually locked
if lockID != "" {
    sm.Unlock(lockID)
    lockID = ""
}

Try / catch

if err := sm.Unlock(id); err != nil {
    if strings.Contains(err.Error(), "state not locked") {
        // benign double-unlock; ignore
    } else {
        return err
    }
}

Prevention

When it happens

Trigger: Calling Unlock twice for the same state (double unlock); calling Unlock when the operation never acquired a lock (e.g. Lock failed earlier but Unlock is invoked unconditionally); calling Unlock after inmem.Reset() cleared the map.

Common situations: Deferred unlock firing after a Lock that returned an error; test teardown that unlocks speculatively; Reset() called mid-suite leaving dangling Unlock calls; logic that assumes Unlock is idempotent.

Related errors


AI-assisted analysis of hashicorp/terraform@c9def3e214 (2026-08-07). Data as JSON: /api/errors/ade159f5c778d3b4. Report an issue: GitHub.