On Terraform 1.15.x, terraform apply can end in a Go stack trace rather than a diagnostic: panic: runtime error: invalid memory address or nil pointer dereference, with readDiff in the frame. It is not your config. It is a dropped map key, and the object that key points at is a live cloud resource that almost none of your tooling can see.

Short version: a failed create_before_destroy leaves an old object in state under a deposed key. If that resource also declares a precondition or postcondition, Terraform 1.15.x panics in readDiff while planning it (hashicorp/terraform#38586). The fix ships in 1.16.0-rc2, released 19 August 2026 and still not GA as of 24 August 2026. Sweep raw state for deposed keys before you upgrade, because 1.16 removes the crash and leaves the orphan.

What actually triggers the readDiff panic

Issue #38586 names the mechanism plainly. The DiffTransformer builds an update node for the deposed object because conditions exist on the resource. It does not propagate the deposed key onto that node. readDiff then asks the plan for a change to the current object, gets nil back, and dereferences it. The reporter filed against Terraform v1.15.2 on darwin_arm64 with AWS provider v6.42.0 and traced the crash to internal/terraform/node_resource_abstract_instance.go:178.

That line matters more than the stack trace does, because it tells you rewriting the precondition expression will not help. The condition's content is irrelevant. Its existence is the trigger.

The fix landed in v1.16.0-alpha20260715 on 15 July 2026 and is carried into v1.16.0-rc2. Every stable Terraform running in production today is on the panicking code path. If a model-generated answer told you this was fixed, check which version it meant.

Terraform's diagnostics are normally better than a raw panic. Even the genuinely annoying cases, like the Terraform "The deprecation originates from" warnings that point at a module which does not own the deprecation, still hand you a message. Here you get a nil dereference in a graph walker, which is the same unhelpful shape as npm's "Cannot read properties of null (edgesOut)" crash: the tool loses a reference mid-walk and takes the process with it. Worse, in CI it usually happens while holding a state lock, under whatever GitHub Actions OIDC role runs your applies, so the next job queues behind a lease nobody released.

How do you confirm a deposed object exists?

One command, and it deliberately bypasses the CLI's own view of state:

terraform state pull > /tmp/state.json
jq -r '.resources[] as $r | $r.instances[]
       | select(.deposed != null)
       | "\($r.type).\($r.name) deposed=\(.deposed) id=\(.attributes.id // "unknown")"' /tmp/state.json

If your state version serializes it differently, grep -n '"deposed"' /tmp/state.json settles the question. Zero hits means the deposed theory is dead and your panic is something else. One or more hits, on a resource that also carries a precondition or postcondition, is #38586.

For the plan side there is a documented path:

terraform show -json tfplan | jq '.resource_changes[] | select(.deposed != null)'
What I actually read. Issue #38586 for the DiffTransformer and readDiff mechanism and the exact source line, and issue #29713 for the missing deposed state variant, where core maintainer jbardin confirms the deposed entry was readable during walkPlanDestroy and gone by walkDestroy. The visibility rules come from HashiCorp's own JSON output format reference.

Three error strings, one dropped deposed key

Operators treat these as one bug. They need different responses.

Error stringIssueWhere it failsWhat to do
panic: ... nil pointer dereference with readDiff in frame#38586Plan graph builds an update node for the deposed object, key not propagatedDrop the condition block for one apply, or move to 1.16.0-rc2
Error: missing deposed state for aws_api_gateway_rest_api.this (4ece7f68)#29713Entry readable in walkPlanDestroy, absent in walkDestroyRe-plan; the deposed entry is real, the destroy walk lost it
panic: MaybeRestoreResourceInstanceDeposed called without DeposedKey#22974Older restore path, reported 2019Historical, same defect class

Seven years of the same failure: the deposed key does not travel with the object it identifies. That is worth knowing before you attribute the crash to your module, your provider, or your backend.

Why terraform state list will never show it

The plan representation and the state representation disagree, and the disagreement is documented. HashiCorp's JSON format reference calls the deposed key "an opaque key representing the specific deposed object" and notes that address plus deposed key together uniquely identify a change. The values representation, which is what terraform show -json gives you for state, says the opposite in plain words: "Only the 'current' object for each resource instance is described. 'Deposed' objects are not reflected in this structure at all."

So your inventory tool, drift detector, cost report and tag-compliance scan all read a document that structurally cannot mention the resource you are paying for.

Config change forces replacementlifecycle create_before_destroyNew object createdappears in state listOld object moved asideunder deposed key 4ece7f68Destroy of old object succeeds?Deposed entry removedstate list and show -json agreeDeposed entry persistscloud resource still running and billedInvisible: terraform state listand show -json values representationVisible: plan JSON deposed fieldand terraform state pull raw JSONOn 1.15.x with a condition blockapply panics in readDiff"yes""no"

There is also no address you can type. The resource addressing reference documents module paths, type and name, numeric count indices and string for_each keys. The word "deposed" does not appear on that page, and the terraform state rm documentation never mentions it either. Every "just state rm the deposed one" answer in circulation describes a grammar that does not exist.

Policy as code inherits the blind spot in a weaker form. The Sentinel tfstate/v2 import exposes a deposed_key attribute on a resource, described as matching the deposed field in resource_changes. It is a flag, not a collection. There is no deposed object whose attributes a policy can evaluate, so a rule like "every instance must carry an owner tag" cannot see the untagged orphan at all. If you are already tracking cloud resources that no identity or team owns, this is a category your governance stack is silently missing.

I have watched a team spend a quarter arguing about an unexplained line item in an AWS bill that turned out to be a deposed NAT gateway from a botched replacement. Their scanner was correct. Their state was correct. The interchange format between them omitted the object.

The documented recovery deletes your only evidence

HashiCorp's support article on create_before_destroy and deposed objects walks operators through removing all instances of the resource from state, importing the wanted object back, then deleting the unwanted one by hand in the cloud console. Its old URL now 302s into IBM's support portal, which tells you how much attention this corner gets.

Step one erases the deposed object's attributes, and that is where the real resource ID was written down. Step three then depends on a human remembering an ID that no longer exists anywhere in the repo, the state, or the plan. If you follow that procedure without recording the ID first, you have converted a tracked orphan into an untracked one.

Related behavior worth naming while you are in here: issue #31635 reports that with create_before_destroy, a failure in a later resource still leaves Terraform destroying the deposed object. Replacement ordering and failure handling around this feature are less deterministic than the lifecycle documentation implies. Deposed leftovers are common, not exotic.

Upgrading to 1.16 makes this quieter, not cleaner

The panic was a loud, unignorable signal that a deposed object existed. On 1.16 the same state applies cleanly and the object is still there, still running, still outside configuration. My read is that most teams will upgrade, watch the error disappear, and never learn the orphan was there. Given the choice I would keep the crash, which is why the jq check belongs in CI before the upgrade rather than after it.

What to run before you move CI to 1.16

Ordered by what bites first.

  1. Sweep every state you own now, while the panic is still there to get your attention. Run the terraform state pull plus jq check above against each workspace. Any hit is a running resource with no configuration governing it.
  2. Record attributes.id, region and ARN in a ticket before any state surgery. Not in terminal scrollback. terraform state rm removes current and deposed instances together, and that command is the moment the ID stops existing.
  3. Prefer letting Terraform destroy it over removing it from state. If the remote object still exists, keep the resource in configuration and let a normal apply plan the destroy of the deposed object. State surgery is the fallback, not the first move.
  4. If that apply panics in readDiff on 1.15.x, comment out the precondition or postcondition block on that one resource, apply, then restore it. This follows from the mechanism in #38586: no conditions means no update node for the deposed object. You are disabling a safety check to clear a leftover, so test against a copy of state first and put the block back in the same pull request.
  5. Hold CI on 1.15.x until 1.16.0 goes GA, then upgrade with the deposed sweep already wired in as a post-apply step. Alerting threshold: a deposed key that survives two consecutive applies is an orphan, not a transient.
  6. Point drift and cost tooling at plan JSON resource_changes[].deposed or at raw state, never at the values representation of terraform show -json, which is documented to omit these objects.

FAQ

What is a deposed object in Terraform? When a resource with create_before_destroy is replaced, Terraform creates the new object first and moves the old one aside under a deposed key, an 8-character hex string such as 4ece7f68. If the destroy of the old object then fails, that entry stays in state and the real cloud resource keeps running.

Why does terraform state list not show deposed objects? No address grammar exists for them, and the values representation of terraform show -json is documented to describe only the current object per instance. Their absence is by design.

Is it safe to run terraform state rm on a deposed resource? There is no syntax to remove only the deposed object, so state rm drops current and deposed together and takes the real resource ID with it. Record the ID first and prefer a planned destroy.

Does Terraform 1.16 remove deposed objects automatically? No. The release candidates fix the nil pointer dereference so the apply completes. The orphan survives the upgrade.