Fix Firestore INTERNAL ASSERTION FAILED (ID: ca9)
Why rapid listener churn can push a Firestore bookkeeping counter below zero, which part of this is actually fixed, and the one mitigation reporters say works when upgrading does not.
TL;DR#
Firestore throws FIRESTORE INTERNAL ASSERTION FAILED: Unexpected state (ID: ca9) CONTEXT: {"ve":-1} when an internal counter that tracks outstanding watch-target responses is decremented one time too many and goes negative. The SDK treats that as unrecoverable and kills its own async queue on purpose, rather than keep running on bookkeeping it no longer trusts. The GitHub issue tracking this ended up documenting more than one distinct cause behind the same assertion ID, found months apart — an earlier one the maintainer referenced as already fixed before this thread even started, then a second one this thread tracked down and fixed, then a third reported afterwards on a release that already contains that fix. Firebase maintainer Mark Duckworth confirmed and fixed the second one — a race where a response for a listener target that was just torn down arrives after a new target has reused the same ID — in pull request 9842, with a regression test attached. That is a genuine, confirmed fix, and it did not end the thread. There is no application-level fix for the SDK-internal race itself; the mitigations developers describe below are a coding pattern that avoids the third trigger, and a reload-based recovery for when the assertion fires anyway.
@firebase/firestore: Firestore (12.3.0): FIRESTORE (12.3.0) INTERNAL ASSERTION FAILED: Unexpected state (ID: ca9) CONTEXT: {"ve":-1}Why it happens#
The original report came from an Angular app (Angular 20.2.4, TypeScript 5.8.3) running Playwright 1.55.0 end-to-end tests against the Firebase Emulator Suite, on Node.js 22.19.0, Windows 11, firestore 12.3.0. The test flow adds a Firestore document, then quickly deletes it, while a listener reacts to the change; roughly 15 to 17 percent of runs crashed instead of redirecting. Duckworth's first question was whether the reporter had seen this before version 12.3.0 — implying an earlier ca9 cause had already been fixed by that release. The reporter confirmed the crash happened on earlier versions too, so this was a separate, still-unidentified bug even though the reporter was already on a version past the first fix.
The thread stayed open for months with nobody able to pin the crash to a specific action, until developer thomaskleiven posted a code sample that triggered it reliably: a Next.js app (Next.js 16, React 19) with reactStrictMode enabled, where one component mounts two Firestore listeners together — one on a path the security rules allow, one on a path they immediately reject with a permission-denied error — and switching to a different UI tab and back unmounts and remounts that component. Strict Mode's development-time double-invocation re-triggers both listeners on that round trip, and the crash appeared. Duckworth confirmed the snippet triggered the crash for him too, intermittently, and, in doing so, distinguished it from the earlier, already-fixed cause: there was now a confirmed-but-unfixed race "in the internal target management, between the SDK and server", and at that point he had no timeline for a fix.
It took roughly four more months for that fix to ship. Duckworth wrote the mechanism into the body of pull request 9842, the change that closed the issue: "The assertion typically occurs when a target is removed and then quickly re-added (or a new target reuses the same SDK target ID). If a response for the previous incarnation of the target arrives after the new target has been created, the SDK may incorrectly process that response as belonging to the new target. This causes the pendingResponses counter to be decremented incorrectly, eventually dropping below zero and triggering the assertion." The PR gives every listen attempt a unique remote target ID, even when the SDK's own target ID is reused across a stop-and-restart cycle, so a stale response from the torn-down target can no longer be misattributed to its replacement. The PR body also records that a regression spec test was added that exercises the same race condition and verifies the fix against it — the strongest kind of proof a maintainer can attach to a flaky bug. Duckworth closed the issue on merging it, and said plainly in doing so: "There have been multiple causes of the ca9 assertion so far. I'm hoping this fix helps all of you continuing to report the assertion."
That warning held. Weeks later, developer Shane Breazeale reported the identical ca9 crash on a release that already contains pull request 9842, with a third, independently-found trigger: a single getDoc() read as the very first Firestore operation on a freshly opened connection. In their account the assertion fires about 60ms after that read resolves — during the read's own target teardown, not during any listener remount — because a watch target-change response arrives with no matching pending request recorded for it. Their report names the exact SDK-internal call pair involved, TargetState.recordTargetResponse asserting the counter stays non-negative inside WatchChangeAggregator.handleTargetChange, which matches the mechanism Duckworth described, just triggered a different way.
Fix#
- Upgrade the
firebasepackage. The fix is inside the Firestore SDK'sRemoteStore, not in anything your code controls, so there is nothing to change on your side besides the dependency version:
npm install firebase@latest
# To check which release first bundled pull request 9842, search the
# Firestore package's own changelog for the PR number:
# https://github.com/firebase/firebase-js-sdk/blob/main/packages/firestore/CHANGELOG.md- Do not treat the upgrade as a guaranteed cure. Pull request 9842 fixes the target-reuse race identified from a listener remount under React Strict Mode. It does not cover the later, third trigger reported on a release that already contains it: a single
getDoc()read as the first Firestore operation on a freshly opened connection. If your crash looks like that — a one-shot read, not a listener you unsubscribe and resubscribe — avoid issuing a transientgetDoc()/getDocs()as the very first call on a newly initialized client. Open a persistentonSnapshot()listener instead and read the first value from its callback; a long-lived target never reaches the teardown step where that report traced the failure.
// Avoid as the first Firestore call on a fresh client:
// await getDoc(doc(db, 'users', uid));
// Use a listener instead, and read the first snapshot from it:
const unsubscribe = onSnapshot(doc(db, 'users', uid), (snap) => {
// first value arrives here; keep the listener open rather than
// tearing it straight back down
});-
Build a reload-based recovery, because there is no other kind once the assertion fires. The SDK's async queue is in a failed state on purpose, and the usual cleanup calls cannot help:
terminate()andclearIndexedDbPersistence()both try to enqueue their own work onto the same dead queue, so they do nothing (and can themselves throw). A global error listener that watches for the stringINTERNAL ASSERTION FAILEDand prompts (or forces) a full page reload is the only way developers in the thread report getting a working Firestore client back in that tab. -
If it still happens after upgrading, Duckworth's own guidance in the thread is to open a new report rather than comment on the closed one, because by his own account this ID has had more than one distinct cause: "The recommended way is to create a new ticket at firebase.google.com/support/troubleshooter/contact and then upload the file there. Reference this GitHub issue … and request the the ticket/log is forwarded to markduckworth@." Attach whatever debug logs you have; one original reporter noted the emulator's own console output showed nothing unusual at the moment of the crash, so the SDK-side stack trace is often the only lead you will have.
Verify the fix#
Confirming you have pull request 9842 only rules out one cause, so verify it like this: check the Firestore changelog for the PR number against your installed firebase version, then re-run whatever triggered the crash — a flaky test suite, a page with several long-lived listeners open for a while — more than a handful of times. A failure rate reported in the 15-to-17-percent range, as in the original issue, is not something one clean run disproves. If your crash was tied to React Strict Mode double-mounting a listener on an unmount/remount cycle, that specific trigger should disappear after the upgrade. If instead it shows up as a one-shot getDoc()/getDocs() call failing shortly after it resolves, with no listener churn involved, you are most likely looking at the third, still-open cause rather than a failed upgrade — the coding-pattern change above, not a newer SDK, is what addresses that one.
One email a month — the latest explained errors
What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.
If the error changes#
Once ca9 fires, the same tab commonly floods with a second assertion, b815, on every Firestore call that follows — that is the async queue reporting it has already failed, not a new bug. It stops the moment you reload; there is nothing to fix in that error on its own.
If instead your crash clears but your tests start failing with Missing or insufficient permissions once the delete-and-redirect logic actually runs, that is an unrelated security-rules problem — see Firestore PERMISSION_DENIED: Every Real Cause and Fix. If you are testing against the emulator and instead see Firestore has already been started and its settings can no longer be changed, that is a separate, well-understood emulator connection issue — see Fix Firestore 'Host has been set in both settings()' error. And if the assertion you are chasing turns out to be CLI-side, during firebase deploy, rather than a runtime crash in the SDK, it is a different bug entirely — see Fix "Assertion failed: resolving hosting target" (Firebase).
Internal assertions like this one are a deliberate design choice, not a bug in the sense of "wrong output": Firestore would rather crash loudly than keep serving a listener whose internal state it no longer trusts. That is a reasonable trade-off for a database client, but it means a single accounting edge case the SDK team has not yet closed can take down an entire tab until the user reloads. If you hit an internal-assertion ID that is not documented anywhere, the realistic next step is the one this thread models: a precise report with your SDK version and whatever stack trace you have, not a workaround in your own code, because the fix — if there is one — has to land inside the SDK.
Further reading: Fix npm ERESOLVE: Firebase prerelease peer conflict.
Frequently Asked Questions
Still stuck? Paste your error.
Paste the error message or stack trace — we check it against every fix and tool on the site.
Continue Reading
Firestore PERMISSION_DENIED: Every Real Cause and Fix
The Firestore error "PERMISSION_DENIED: Missing or insufficient permissions" never tells you which rule rejected you. Here is how to find out — and the seven causes that account for almost every occurrence.
Fix 'Failed to get document because the client is offline'
The emulator call from the Stack Overflow question, reproduced with the Node SDK against a real Firestore emulator: what the SDK builds from the host string, which other causes give the same error, and which do not.
Fix Firestore 'Host has been set in both settings()' error
Why Next.js reloads turn a harmless emulator warning into a hard FirebaseError, what changed in the modular SDK, and a setup that survives every reload.


