hashicorp/terraform · error · LockInfo

Lock Info: ID: {{.ID}} Path: {{.Path}} Ope

Error message

Lock Info:
  ID:        {{.ID}}
  Path:      {{.Path}}
  Operation: {{.Operation}}
  Who:       {{.Who}}
  Version:   {{.Version}}
  Created:   {{.Created}}
  Info:      {{.Info}}

What it means

This is the multi-line template rendered by LockInfo.String() and returned (via LockInfo.Err()) when state-lock acquisition fails because another process holds the lock. The template emits ID, Path, Operation, Who (user@host), Version, Created, and Info of the competing lock holder so the user knows who/what is blocking them. The placeholders {{.ID}} etc. are filled from the LockError.Info captured by the failed Lock call.

Source

Thrown at internal/states/statemgr/locker.go:176

	// don't error out on user and hostname, as we don't require them
	userName := ""
	if userInfo, err := user.Current(); err == nil {
		userName = userInfo.Username
	}
	host, _ := os.Hostname()

	info := &LockInfo{
		ID:      id,
		Who:     fmt.Sprintf("%s@%s", userName, host),
		Version: version.Version,
		Created: time.Now().UTC(),
	}
	return info
}

// Err returns the lock info formatted in an error
func (l *LockInfo) Err() error {
	return errors.New(l.String())
}

// Marshal returns a string json representation of the LockInfo
func (l *LockInfo) Marshal() []byte {
	js, err := json.Marshal(l)
	if err != nil {
		panic(err)
	}
	return js
}

// String return a multi-line string representation of LockInfo
func (l *LockInfo) String() string {
	tmpl := `Lock Info:
  ID:        {{.ID}}
  Path:      {{.Path}}
  Operation: {{.Operation}}
  Who:       {{.Who}}

View on GitHub (pinned to d32a084675)

Solutions

  1. Wait for the other operation to finish, then retry — LockWithContext already backs off and retries until the context deadline.
  2. If the other holder is genuinely dead (crashed machine), use `terraform force-unlock <ID>` with the ID shown in the rendered lock info.
  3. Verify the Who/Path fields to confirm the holder is not your own stale process before force-unlocking.
  4. Do NOT force-unlock during an active cooperative run — it can corrupt state.

Example fix

// before
id, err := statemgr.LockWithContext(ctx, mgr, info)
if err != nil {
    return err // shows raw template-ish error
}

// after
id, err := statemgr.LockWithContext(ctx, mgr, info)
if err != nil {
    var le *statemgr.LockError
    if errors.As(err, &le) && le.Info != nil {
        return fmt.Errorf("state is locked by another run:\n%s\nif that run is gone, run: terraform force-unlock %s", le.Info.String(), le.Info.ID)
    }
    return err
}
Defensive patterns

Strategy: try-catch

Validate before calling

n/a — lock conflict depends on remote state; pre-check by attempting Lock.

Type guard

func isLockError(err error) (*statemgr.LockError, bool) {
    var le *statemgr.LockError
    if errors.As(err, &le) {
        return le, true
    }
    return nil, false
}

Try / catch

id, err := statemgr.LockWithContext(ctx, mgr, info)
if le, ok := isLockError(err); ok && le.Info != nil {
    return fmt.Errorf("state locked by %s@%s (id %s); run `terraform force-unlock %s` if stale", le.Info.Who, le.Info.Path, le.Info.ID, le.Info.ID)
}

Prevention

When it happens

Trigger: A Locker.Lock call returns a *LockError whose Info is the existing lock's metadata; LockWithContext or the CLI formats it via LockInfo.String()/Err() to present the conflict to the user.

Common situations: Two Terraform runs (apply/plan/destroy) targeting the same state concurrently; a previous run crashed without releasing the lock; a CI job and a developer running locally against the same backend; a long-running apply still in progress.

Related errors


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