Repository navigation
[firebase_auth] getIdTokenResult() throws an uncatchable fatal exception on Android when the native token refresh fails on a background thread #18561
Description
Activity
- addedNeeds AttentionThis issue needs maintainer attention.This issue needs maintainer attention.platform: androidIssues / PRs which are specifically for Android.Issues / PRs which are specifically for Android.type: bugSomething isn't workingSomething isn't workingtype: crashA compile error or crashA compile error or crash
on Aug 12, 2026 Hi @luanbatistadenga, thanks for the detailed report. I'm looking into it.
Hi @luanbatistadenga, thanks again for the detailed report and Crashlytics stacks.
I've dug into this against the current
firebase_auth/ Crashlytics code paths, and here's what that stack is telling me.The top frame is
io.flutter.plugins.firebase.crashlytics.FlutterErrorwith the... Error thrown .suffix. That type/message is created when a Dart error is recorded into Crashlytics (viarecordError/recordFlutterFatalErrorwithfatal: true) — it is not a native Firebase Auth abort onFirebase Background Thread #3.The frames below it are the normal pigeon path:
_extractReplyValueOrThrow→FirebaseAuthUserHostApi.getIdToken→MethodChannelUser.getIdTokenResult→ yourid/authHandleSo the platform reply did reach Dart and was turned into a
FirebaseAuthException. Crashlytics then recorded that Dart failure as a fatal event (which is why the console shows it as a crash). TheFirebase Background Thread #Nlabel on these Flutter fatal reports is often Crashlytics/Firebase processing, not proof that Auth crashed that native thread.This pattern usually means:
- The Auth failure became an
uncaught asyncerror (for example theFuturefromgetIdTokenResult/ youridgetter wasn’t awaited inside thetry, or it escaped the zone), and - A global handler recorded it as fatal — commonly:
PlatformDispatcher.instance.onError = (error, stack) { FirebaseCrashlytics.instance.recordError(error, stack, fatal: true); return true; };
and/or
FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError.In that setup, Crashlytics will correctly show a fatal crash even though this isn’t a native process-killing Auth crash. A normal
await getIdTokenResult(...)insidetry/catchshould be catchable; ifcatchnever runs, the Future likely never failed inside thattry.Could you double-check that
authHandleactuallyawaitsid/getIdTokenResultinside thetry, and share how Crashlytics is wired inmain()?- The Auth failure became an
- addedblocked: customer-responseWaiting for customer response, e.g. more information was requested.Waiting for customer response, e.g. more information was requested.and removedNeeds AttentionThis issue needs maintainer attention.This issue needs maintainer attention.type: crashA compile error or crashA compile error or crash
on Aug 12, 2026 You were right, and this was a useful correction — thank you.
I traced every call site of the
idgetter in our app._AppState.authHandle(the one in the original report) does correctlyawaitinside itstry, and itscatchblock already classifies both observed message variants (a network-connect failure and a revoked-session failure) without ever reaching a fatal report.The actual source turned out to be a different, unrelated call site:
UserStore.clearCurrentUserCache(), which is invoked withoutawaitfrom a failure handler, and had no try/catch around its own call togetIdTokenResult. Since nothing awaits that Future, any rejection there is a genuinely uncaught async error with no chance for any local catch to intercept it — it goes straight to our global zone handler, which reports everything that reaches it as fatal with no classification. That fully explains why both message variants ended up unclassified: they never went throughauthHandle's catch at all.So: not a native-thread escape in the plugin, just an ordinary unawaited-Future bug in our own code. Fixed here: denga-app/mobile#434. Leaving this issue open only as background evidence that this general failure class (an Auth call failing during a background/refresh path) exists in the wild — no ask on your end beyond what's already here.
- addedNeeds AttentionThis issue needs maintainer attention.This issue needs maintainer attention.and removedblocked: customer-responseWaiting for customer response, e.g. more information was requested.Waiting for customer response, e.g. more information was requested.
on Aug 12, 2026 Hi @luanbatistadenga, thanks for confirming. Closing issue as it doesn't require a fix on our end.
- addedresolution: userThis was a user issue, e.g. invalid configuration or code.This was a user issue, e.g. invalid configuration or code.and removedNeeds AttentionThis issue needs maintainer attention.This issue needs maintainer attention.
on Aug 12, 2026 - locked and limited conversation to collaborators
on Sep 11, 2026
Steps to Reproduce
We don't have a fully deterministic local repro — this surfaced in production via Crashlytics, and matches the same general class of bug as the closed #11311 ("Crash when calling user.getIdToken(true)"), but with a different (also uncaught) underlying
FirebaseAuthException.Useron Android.user.getIdTokenResult(true)(force refresh) from a Darttry/catchblock.securetoken.googleapis.com, or because the refresh token itself is no longer valid (user-token-expired/ a generic[firebase_auth/unknown]"The user's credential is no longer valid. The user must sign in again." message).Expected results
The
FirebaseAuthExceptionshould be catchable by the surrounding Darttry/catch, the same way it is for a normal awaitedFuturerejection.Actual results
The exception surfaces as an uncaught, fatal crash, even though the call site has an enclosing
try/catch(and, in our case, correct typed classification of the exception's.code/.messagein thecatchblock — none of which ever runs).Crashlytics-reported stack trace (thread name is notable — the exception originates on a Firebase-managed background thread, not the calling Dart isolate/await chain):
A second variant of the exact same issue (same blame frame,
FirebaseUserExtension.id) shows the network-failure flavor instead:Root cause (hypothesis)
The call site is wrapped like this:
...called from inside a
try { ... } catch (exception, s) { ... }block that already has correct, specific handling for this exactFirebaseAuthException(matching both typed.codes likeuser-token-expiredand, as a fallback for the genericunknowncode, the message text "credential is no longer valid" / "must sign in again"). Thatcatchblock never runs for these events — the exception is fatal at the OS/engine level instead.Given the crashing thread is named "Firebase Background Thread #3" (not the Flutter UI/platform thread), this looks like the same general category of bug we independently found and reported for
google_sign_in_androidin #190939: a native SDK operation completes/errors on its own background thread, and the plugin's platform-channel reply path does not correctly hop back through the awaited DartFuture's normal error-propagation path, so the Dart-sidetry/catcharound theawaitnever gets a chance to run.Mitigation applied on our side
We removed the forced refresh (
getIdTokenResult(true)→getIdTokenResult()), since the underlying custom claim we read (apiId) is set once at account-creation time server-side and does not need an unconditional forced refresh on every auth-state change. This reduces how often this code path triggers a native token refresh at all, but does not fix the underlying issue: any code that legitimately needsforceRefresh: true(or that hits this during Android's normal background token refresh) can still hit an uncatchable fatal crash.Versions
firebase_auth: 6.5.4flutter doctor -v