{"record":{"id":"bf973df4d8b9f952","repo":"hashicorp/terraform","slug":"failed-to-delete-state-file-v-v","errorCode":null,"errorMessage":"Failed to delete state file %v: %v","messagePattern":"Failed to delete state file (.+?): (.+?)","errorType":"console","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/backend/remote-state/gcs/client.go","lineNumber":88,"sourceCode":"\t\t}\n\t\tif _, err := stateFileWriter.Write(data); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn stateFileWriter.Close()\n\t}()\n\tif err != nil {\n\t\treturn diags.Append(fmt.Errorf(\"Failed to upload state to %v: %v\", c.stateFileURL(), err))\n\t}\n\n\treturn diags\n}\n\nfunc (c *remoteClient) Delete() tfdiags.Diagnostics {\n\tvar diags tfdiags.Diagnostics\n\n\tctx := context.TODO()\n\tif err := c.stateFile().Delete(ctx); err != nil {\n\t\treturn diags.Append(fmt.Errorf(\"Failed to delete state file %v: %v\", c.stateFileURL(), err))\n\t}\n\n\treturn diags\n}\n\n// Lock writes to a lock file, ensuring file creation. Returns the generation\n// number, which must be passed to Unlock().\nfunc (c *remoteClient) Lock(info *statemgr.LockInfo) (string, error) {\n\tctx := context.TODO()\n\n\t// update the path we're using\n\t// we can't set the ID until the info is written\n\tinfo.Path = c.lockFileURL()\n\n\tinfoJson, err := json.Marshal(info)\n\tif err != nil {\n\t\treturn \"\", err\n\t}","sourceCodeStart":70,"sourceCodeEnd":106,"githubUrl":"https://github.com/hashicorp/terraform/blob/c9def3e214014c1188faabfc4a5bde5095139765/internal/backend/remote-state/gcs/client.go#L70-L106","documentation":"remoteClient.Delete() calls stateFile().Delete(ctx) to remove the .tfstate object; any error (object not present is still an error here, unlike Get) is wrapped with the gs:// URL. Triggered by DeleteWorkspace after the name passes validation, and by explicit state-file deletion.","triggerScenarios":"'terraform workspace delete <ws>' removing its state object; the object was already deleted by another process (returns notFound); SA lacks storage.objects.delete; bucket has a retention policy holding the object.","commonSituations":"Idempotent workspace deletion racing with another teardown; SA has objectAdmin for create/get but not delete; retention policy / object holds blocking deletion.","solutions":["If %v is 'notFound', the workspace is already gone — treat as success in automation (no-op).","Grant SA 'roles/storage.objectAdmin' (includes delete) or a custom role with storage.objects.delete.","Remove bucket retention policy / object holds blocking the delete, or wait for retention to elapse.","Retry; transient 5xx during delete is possible."],"exampleFix":"// before: SA lacks delete\nroles/storage.objectCreator only\n\n// after\ngsutil iam ch serviceAccount:tf@proj.iam.gserviceaccount.com:roles/storage.objectAdmin gs://bucket\nterraform workspace delete <ws>","handlingStrategy":"try-catch","validationCode":"// Pre-check object existence to make delete idempotent\nctx := context.Background()\n_, err := client.Bucket(bucket).Object(statePath).Attrs(ctx)\nif err == storage.ErrObjectNotExist { return nil }","typeGuard":"import \"strings\"\nfunc isAlreadyGone(err error) bool { return strings.Contains(err.Error(), \"notFound\") || err == storage.ErrObjectNotExist }","tryCatchPattern":"if diags := client.Delete(); diags.HasErrors() {\n    if isAlreadyGone(diags.Err()) { return nil }  // idempotent\n    return diags\n}","preventionTips":["Make workspace-deletion automation idempotent: treat 'notFound' as success.","Grant SA storage.objects.delete.","Avoid retention policies on state buckets or account for them in teardown."],"tags":["gcs","gcp","state","delete","iam","retention"],"analyzedSha":"c9def3e214014c1188faabfc4a5bde5095139765","analyzedAt":"2026-08-07T15:39:49.278Z","schemaVersion":2},"datasetVersion":"2026-08-07T21:17:07.882Z"}