skip to content

In a Flutter app using FlutterFire, why must main() await Firebase.initializeApp before runApp, and what breaks when that order slips?

level: juniorimportance: must knowfreq 72%

answer

  1. startup order inside main()
  2. binding before any plugin channel call
  3. DefaultFirebaseOptions.currentPlatform
  4. Firebase.app() throws core/no-app
  5. same options again returns the existing app

basics

~10 s

Every FlutterFire plugin looks up a FirebaseApp that exists only after Firebase.initializeApp completes. So main() calls WidgetsFlutterBinding.ensureInitialized(), awaits initializeApp with DefaultFirebaseOptions.currentPlatform, then calls runApp; otherwise the first plugin call throws [core/no-app].

solid answer

~40 s

`Firebase.initializeApp` registers the default `FirebaseApp`, and every plugin getter depends on it: `FirebaseAuth.instance` calls `Firebase.app()`, which throws `[core/no-app] No Firebase App '[DEFAULT]' has been created` until initialization completes. The call goes over a platform channel before `runApp` has created the binding, so `main` first calls `WidgetsFlutterBinding.ensureInitialized()`; without it a debug build fails with "Binding has not yet been initialized." Then you `await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform)` and only then `runApp`. The order usually slips through a missing `await`, initialization moved into a root widget's `initState`, or a service that reads `FirebaseFirestore.instance` before the awaited line. Calling it again with the same options — as a hot restart does — returns the existing app rather than throwing.

code

dart · 21 lines
dart
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';

import 'firebase_options.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  runApp(const VolunteerApp());
}

class VolunteerApp extends StatelessWidget {
  const VolunteerApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(home: Scaffold(body: Text('Shifts')));
  }
}

go deeper

for a junior

Recite the three lines in order: ensureInitialized, await initializeApp with DefaultFirebaseOptions.currentPlatform, then runApp. Know the no-app error message when you see it.

for a middle

Explain why the binding must exist first (initializeApp is a platform-channel call) and why plugin getters such as FirebaseAuth.instance depend on the registry that initializeApp fills.

for a senior

Find the ordering bug in a real codebase: an unawaited call, eager DI wiring, or a second entry point. Know that repeated initialization with matching options is safe across hot restarts.

for a principal

Decide whether startup blocks on Firebase or renders first and gates features behind the future, and set the rule every module that touches a plugin at startup must follow.

## What `Firebase.initializeApp` sets up FlutterFire is the family of Flutter plugins — `firebase_core`, `firebase_auth`, `cloud_firestore`, `firebase_messaging` and the rest — that wrap the native Firebase SDKs on Android and Apple platforms and the Firebase JavaScript SDK on the web. `firebase_core` owns a single concept the others depend on: the **`FirebaseApp`**, a named bundle of `FirebaseOptions` (the API key, app ID, messaging sender ID and project ID are required; the storage bucket, auth domain, iOS bundle ID and a few more are optional). Every other plugin hangs off an app. In the pinned `firebase_auth` source, the `FirebaseAuth.instance` getter is literally `FirebaseAuth.instanceFor(app: Firebase.app())`. `Firebase.app()` looks the default app — named `[DEFAULT]` — up in a Dart-side registry, and that registry is filled only by `Firebase.initializeApp`. On its first call, `initializeApp` asks the native side which Firebase apps already exist, copies them into the registry, and then creates or confirms the default app. Until that `Future` completes, the lookup throws a `FirebaseException` whose text reads: `[core/no-app] No Firebase App '[DEFAULT]' has been created - call Firebase.initializeApp()` ## The canonical `main` The FlutterFire setup guide prescribes one order, and every step has a reason: 1. **Make `main` asynchronous** — `Future<void> main() async` — so it can await. 2. **Call `WidgetsFlutterBinding.ensureInitialized()`** before any plugin call. 3. **`await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform)`**, where `DefaultFirebaseOptions` comes from the `firebase_options.dart` file that `flutterfire configure` generated. 4. **Only then call `runApp`**, so no widget can reach a plugin before the default app exists. ```dart import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/material.dart'; import 'firebase_options.dart'; Future<void> main() async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp( options: DefaultFirebaseOptions.currentPlatform, ); runApp(const VolunteerApp()); } ``` ## Why the binding comes first `Firebase.initializeApp` is not pure Dart: `firebase_core` reaches its Kotlin and Swift code through Pigeon-generated platform channels. A channel on the root isolate sends through `ServicesBinding.instance.defaultBinaryMessenger`, and that binding normally comes into existence inside `runApp`. Because Firebase has to be ready *before* `runApp`, `main` creates the binding itself with `WidgetsFlutterBinding.ensureInitialized()`. The call is idempotent, so the later `runApp` simply reuses it. Skip it and a debug build stops with the framework's **"Binding has not yet been initialized."** error, whose hint names exactly this call. ## How the order slips in real code The failure is rarely a missing `initializeApp`; it is an ordering accident around one: - **A missing `await`.** `Firebase.initializeApp(...)` fired and forgotten lets `runApp` build the first frame while the registry is still empty; the first widget or service that reads `FirebaseAuth.instance` throws `[core/no-app]`. - **Initializing inside the widget tree.** Calling `initializeApp` from a root widget's `initState` means its children may already have built and touched a plugin. - **Eager service wiring.** A dependency-injection setup or a service constructor that reads `FirebaseFirestore.instance` before the awaited line runs fails the same way. Dart top-level variables are initialized lazily, on first read, so a top-level `final db = FirebaseFirestore.instance;` is only a problem if something reads it too early. - **A second Dart entry point.** Code that runs in another isolate, such as a messaging background handler, starts with an empty registry and must initialize Firebase itself. ## Calling it more than once `initializeApp` is safe to call again with the same options, which matters because a **hot restart** wipes Dart state but not the native Firebase apps. The default-app branch behaves like this in the pinned `firebase_core`: | Situation when `initializeApp` runs | Result | |---|---| | No default app yet, options passed | Creates `[DEFAULT]` from the options | | Default app exists, options match on the checked fields | Returns the existing app | | Default app exists, `apiKey` (or a set `databaseURL` / `storageBucket`) differs | Throws `[core/duplicate-app]` | | No default app, no options, Android | Reads options from the Android resources built from `google-services.json`, and fails if they are missing | | No default app, no options, Apple platform without a bundled plist | Throws `[core/not-initialized]` | ## A checklist when `[core/no-app]` appears - **Is `main` async and is the call awaited?** An unawaited `initializeApp` is the most common cause. - **Does anything read a plugin instance before the awaited line?** Look at service locators, singletons constructed in `main`, and static initializers that are touched early. - **Is the failing code in another isolate?** Each isolate has its own Dart registry and needs its own initialization. - **Is it a test?** A widget test runs without the native side, so Firebase has to be replaced with fakes rather than initialized. So "initialize twice and you always crash" is wrong, and so is "initialize once, anywhere, and it will be ready in time". The rule is narrower: **await it once, before `runApp`, with the options for the platform you are running on.**

  • What happens on a hot restart, when main() calls Firebase.initializeApp a second time?
    A hot restart resets Dart state but not the native Firebase apps. On its first call after the restart, `firebase_core` pulls the existing native apps back into its Dart registry, then compares the options you pass with the default app's `apiKey` (and `databaseURL` / `storageBucket` when set). If they match it returns the existing app; if they differ it throws `[core/duplicate-app]`.
  • Could you call runApp first and show a loading screen while Firebase initializes?
    Yes: create the initialization future once, outside `build`, and render a loading widget until it completes, making sure nothing below reads a plugin instance before then. It rarely pays off, because the native launch screen already covers the short await in `main`, and the blocking version cannot leak an uninitialized read.

It is like switching on the building's power before the tenants move in: a tenant who arrives first (a widget reading FirebaseAuth.instance) finds a dead socket, not a slower one.

saying these in an interview costs you the question

  • Call runApp first and initialize Firebase later in the first screen's initState.
  • WidgetsFlutterBinding.ensureInitialized() is only needed inside widget tests.
  • Firebase.initializeApp is fast, so it does not need to be awaited.
  • FirebaseAuth.instance initializes the default Firebase app by itself on first use.
  • Calling Firebase.initializeApp twice always throws a duplicate-app error.