In a Flutter app using firebase_auth, why can FirebaseAuth.instance.currentUser be null at launch for a user who is still signed in?
answer
- snapshot versus stream
- two reasons for null
- session restored from local storage
- first event after restoration
- persistence configurable on web only
basics
~20 scurrentUser is a snapshot, and at launch firebase_auth may still be restoring the persisted session, so it reads null. The first authStateChanges() event arrives only after restoration; route from it, or await authStateChanges().first, instead of reading currentUser early.
solid answer
~40 s`currentUser` is documented as a snapshot, and the FlutterFire guide lists two reasons it can be `null`: nobody is signed in, or the auth object has not finished initializing. At launch the SDK first restores stored credentials — on Android and iOS the session is always persisted on the device — so an early read can see `null` for a user who is still signed in. The streams avoid the race: their first event fires after the credentials have been restored. So the app shows a neutral splash until `authStateChanges()` emits, routes from that event, and uses `await FirebaseAuth.instance.authStateChanges().first` when it needs a one-off answer. After that, `currentUser` is fine for reading `uid`, but not for deciding whether an account is still valid, which needs `reload()`.
code
dart · 10 linesimport 'package:firebase_auth/firebase_auth.dart';
// Wrong: may read null before the persisted session is restored.
bool isSignedInSnapshot() => FirebaseAuth.instance.currentUser != null;
// Right: the first stream event arrives after restoration.
Future<bool> isSignedInAtLaunch() async {
final User? user = await FirebaseAuth.instance.authStateChanges().first;
return user != null;
}go deeper
Remember that currentUser is a snapshot that can be null during startup, and that the auth streams give the real initial state.
Explain session restoration from local storage, why the first stream event comes after it, and how per-platform persistence differs between mobile and web.
Fix launch-time flicker and false sign-outs by gating on the first event, and know when a stale non-null user needs reload() to be trusted.
Set one app-wide rule for reading auth state at launch so no feature reintroduces snapshot reads that race session restoration.
## The symptom A cook signs in to the recipe-sharing app, closes it, and opens it the next morning. The app's first screen checks `FirebaseAuth.instance.currentUser`, finds `null`, and shows the sign-in screen for a moment — or for good — even though the session is still valid. This is one of the most common `firebase_auth` bugs, and it comes from treating a snapshot as if it were a state machine. ## What `currentUser` actually is The pinned source documents `currentUser` as returning the current `User` if signed in, or `null` if not, and adds that "this getter only provides a snapshot of user state"; apps that need to react should use the streams instead. The FlutterFire "Manage users" guide spells out why a snapshot can mislead: `currentUser` can be `null` for **two** reasons — 1. the user is not signed in, or 2. **the auth object has not finished initializing**. Initialization here means restoring the persisted session. At launch the SDK reads the stored credentials from local storage before it knows who, if anyone, is signed in. Code that reads `currentUser` in the first moments of the app can land in that window and see `null` for a user who is, in fact, signed in. ## How persistence works per platform | Platform | Where the session lives | Configurable? | |---|---|---| | Android, iOS | On the device, managed by the native SDK | No — always persisted across restarts; clearing app data wipes it | | Web | IndexedDB by default | Yes: `Persistence.LOCAL`, `INDEXED_DB`, `SESSION` or `NONE`, via `FirebaseAuth.instanceFor(app:, persistence:)` or `setPersistence()`, which is web-only | So on mobile the session *is* there; it is just not loaded yet when the snapshot is read. ## The fix: wait for the first event The guide describes the streams' startup behaviour precisely: "When the app starts, an event fires after the user credentials (if any) from local storage have been restored, meaning that your listeners always get called when the user state is initialized." That first event is the reliable answer to "who is signed in at launch?": - **Route from the stream, not the snapshot.** Let `authStateChanges()` decide between the sign-in screen and the recipe feed; show a neutral splash until its first event arrives. - **Need a one-off answer at startup?** `await FirebaseAuth.instance.authStateChanges().first` resolves with the restored user, or `null`, once initialization has finished. - **Use `currentUser` only after that point**, and only where a snapshot is enough — reading `uid` to build a query when you already know the user is signed in, for instance. ```dart import 'package:firebase_auth/firebase_auth.dart'; Future<bool> hasRestoredSession() async { final User? user = await FirebaseAuth.instance.authStateChanges().first; return user != null; } ``` ## The startup sequence, step by step 1. `main` awaits `Firebase.initializeApp` and calls `runApp`. 2. The first frame shows a splash, because no auth event has arrived yet. 3. The SDK restores the stored session from local storage. 4. `authStateChanges()` emits the restored `User`, or `null` if there was none. 5. The gate replaces the splash with the recipe feed or the sign-in screen. Any read of `currentUser` between steps 1 and 4 is the race. ## Things that do not cause it Several plausible-sounding explanations are wrong, and interviewers listen for them: - **"The token expired overnight."** An expired ID token is refreshed by the SDK with the stored refresh token; it does not sign the user out. A signed-out state comes from an explicit sign-out, a revoked or expired refresh session, or deleted app data. - **"Mobile doesn't persist sessions by default."** It always does; persistence is only configurable on web. - **"Hot restart logs the user out."** A hot restart reruns Dart code, and the same restore-then-emit sequence applies; the user reappears on the first event. ## A related trap: signed in, but stale The opposite mistake is trusting `currentUser` too much. The object can be signed in but out of date: a display name changed on another device, or an account disabled by an administrator, does not show up until the app calls `currentUser!.reload()`. For a disabled or deleted account, that `reload()` throws a `FirebaseAuthException` with `user-disabled` or `user-not-found`. Snapshot reads are therefore fine for identity (`uid`), not for decisions about whether the session is still valid.
- Can you turn off session persistence on Android or iOS for a shared kiosk device?Not through firebase_auth: on native platforms persistence is not configurable and the session is always stored on the device. `setPersistence()` and the `persistence` argument of `FirebaseAuth.instanceFor` apply to web only. On a kiosk you call `signOut()` explicitly at the end of each session.
- When is reading currentUser the right choice?After the first auth event has arrived and you only need identity, such as the `uid` for building a Firestore path. It is a synchronous, cheap snapshot. It is the wrong input for routing or for deciding that an account is still valid, which needs the stream or `reload()`.
saying these in an interview costs you the question
- The ID token expired overnight, so firebase_auth signed the user out.
- Android and iOS do not persist Firebase sessions unless setPersistence is called.
- currentUser is always ready as soon as Firebase.initializeApp completes.
- authStateChanges() only emits on later sign-ins, so it cannot answer the launch question.
- A non-null currentUser proves the account is still enabled on the server.