hashicorp/terraform · error · statemgr.LockError

writing %q failed: %v

Error message

writing %q failed: %v

What it means

remoteClient.Lock() writes the .tflock object with a DoesNotExist precondition (so only one writer can win). If Write or Close on that conditional writer fails, the error is wrapped through lockError into a *statemgr.LockError. Most commonly this is the precondition failure meaning someone else already holds the lock; it also fires on permission/quota/network errors.

Source

Thrown at internal/backend/remote-state/gcs/client.go:118

	// we can't set the ID until the info is written
	info.Path = c.lockFileURL()

	infoJson, err := json.Marshal(info)
	if err != nil {
		return "", err
	}

	lockFile := c.lockFile()
	w := lockFile.If(storage.Conditions{DoesNotExist: true}).NewWriter(ctx)
	err = func() error {
		if _, err := w.Write(infoJson); err != nil {
			return err
		}
		return w.Close()
	}()

	if err != nil {
		return "", c.lockError(fmt.Errorf("writing %q failed: %v", c.lockFileURL(), err))
	}

	info.ID = strconv.FormatInt(w.Attrs().Generation, 10)

	return info.ID, nil
}

func (c *remoteClient) Unlock(id string) error {
	ctx := context.TODO()

	gen, err := strconv.ParseInt(id, 10, 64)
	if err != nil {
		return fmt.Errorf("Lock ID should be numerical value, got '%s'", id)
	}

	if err := c.lockFile().If(storage.Conditions{GenerationMatch: gen}).Delete(ctx); err != nil {
		return c.lockError(err)
	}

View on GitHub (pinned to c9def3e214)

Solutions

  1. If the wrapped error indicates precondition/412, another process holds the lock: wait for it to finish or 'terraform force-unlock <ID>' only if it's truly stale.
  2. Confirm SA has storage.objects.create on the bucket (needed to create .tflock).
  3. Check the existing lock: 'gsutil cat gs://<bucket>/<prefix>/<ws>.tflock' to see who holds it (LockInfo with Operation, Who, Created).
  4. Prevent concurrency: serialize CI for a given workspace; use distinct workspaces for parallel pipelines.

Example fix

# recover from stale lock
gsutil cat gs://bucket/prefix/default.tflock   # inspect holder
terraform force-unlock 1681234567890   # generation-based ID
terraform apply
Defensive patterns

Strategy: try-catch

Validate before calling

// Pre-check whether a lock exists
ctx := context.Background()
_, err := client.Bucket(bucket).Object(lockPath).Attrs(ctx)
if err == nil { return fmt.Errorf("workspace already locked; check .tflock") }

Try / catch

_, err := client.Lock(info)
if le, ok := err.(*statemgr.LockError); ok {
    // inspect le.Info to surface holder; offer force-unlock with the generation
}

Prevention

When it happens

Trigger: 'terraform apply' / init on a fresh workspace where the .tflock already exists (another apply/plan/init is running) → precondition 412; or SA lacks storage.objects.create on the prefix; or transient GCS error during the lock write.

Common situations: Concurrent CI jobs on the same workspace; a crashed prior run left the .tflock behind; SA missing create permission on the lock path; someone is running a long apply.

Related errors


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