hashicorp/terraform · error · LockError

writing failed

Error message

writing %q failed: %v

What it means

Thrown by the remote client Lock when writing the lock file fails. Lock uses a precondition (storage.Conditions{DoesNotExist: true}) so the write is rejected if a lock file already exists; any other write/close error is also wrapped here via c.lockError.

Solutions

  1. If another run legitimately holds the lock, wait for it to finish or run `terraform force-unlock <id>`.
  2. Inspect the returned LockError.Info for the existing lock's owner and ID.
  3. Grant objects.create permission on the bucket (lock files live alongside state).
  4. Add a pre-run check that no stale lock exists, and clean it up deliberately.

Example fix

// recovery
$ terraform force-unlock <existing-lock-id>
# then retry
terraform apply
Defensive patterns

Strategy: validation

Validate before calling

// Pre-flight: detect an existing lock file before attempting to acquire.
// gsutil stat gs://bucket/<prefix>default.tflock && echo "LOCK HELD"

Type guard

func isPreconditionFailed(err error) bool {
    var ge *googleapi.Error
    return errors.As(err, &ge) && ge.Code == 412
}

Try / catch

// On 412, surface existing lock info and instruct force-unlock.
if err := c.Lock(info); err != nil {
    if isPreconditionFailed(err) {
        return fmt.Errorf("state already locked; run terraform force-unlock")
    }
    return err
}

Prevention

When it happens

Trigger: lockFile.If(DoesNotExist:true).NewWriter write or close fails — most commonly a 412 Precondition Failed because another process holds the lock, but also transport errors or missing objects.create permission on the lock path.

Common situations: Two CI jobs racing to acquire the lock; a previous terraform run crashed leaving a stale lock; service account cannot create the lock object.

Related errors


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

Appendix: 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 d32a084675)