skip to content

In Angular, when several provideAppInitializer functions return Promises, do they run one after another or in parallel, and when does rendering start?

level: middleimportance: should knowfreq 38%

answer

  1. called in provider order
  2. async parts overlap
  3. all awaited together
  4. environment initializers come first
  5. chain dependencies yourself

basics

~10 s

Angular calls each initializer synchronously in provider order, then waits for all returned Promises and Observables together, so their async work overlaps. The root component is created only after every one has settled successfully.

solid answer

~40 s

Angular's `ApplicationInitStatus` loops over the registered initializers in provider order and calls each one in an injection context. It does not wait between calls: it collects every returned Promise, converts every Observable into a Promise that resolves on complete, and awaits them all together. So the synchronous parts run in order, the async parts run concurrently, and one slow initializer does not delay the start of the next. When all have resolved, Angular sets the locale and creates the root component; if any rejects, bootstrap fails. Environment initializers run earlier still, when the root injector is created. If initializer B needs A's result, B must await A explicitly — or both steps belong in one initializer.

code

ts · 17 lines
ts
import { ApplicationConfig, inject, provideAppInitializer } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';
import { ConfigService } from './config.service';
import { FeatureFlags } from './feature-flags';

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(),
    // One initializer, explicit sequence: flags need the API URL from the config
    provideAppInitializer(async () => {
      const config = inject(ConfigService);
      const flags = inject(FeatureFlags);
      await config.load();
      await flags.load(config.apiUrl);
    }),
  ],
};

go deeper

for a junior

Recall that nothing renders until every initializer has finished, and that Promises and Observables are both awaited.

for a middle

Explain calls in provider order with concurrent waiting, and why environment initializers run before application initializers.

for a senior

Diagnose dependent initializers racing each other and fix them with one initializer or an explicit await instead of reordering providers.

for a principal

Treat startup as a critical path: keep independent work parallel and push anything non-essential out of initializers entirely.

## The phases of standalone bootstrap When `bootstrapApplication(App, config)` runs, the startup work relevant to initializers happens in this order (assuming Angular v22.2): 1. The **root environment injector** is created from `ApplicationConfig.providers`. 2. **Environment initializers** registered with `provideEnvironmentInitializer()` run, synchronously, as part of resolving that injector. 3. **Application initializers** registered with `provideAppInitializer()` are called. 4. Angular waits until every async result from step 3 has settled. 5. The `LOCALE_ID` is applied and the **root component** is created and attached, which starts rendering. Module-based apps started with `bootstrapModule` follow the same sequence with the module's injector in place of the standalone one. ## How step 3 and step 4 actually work The logic lives in `ApplicationInitStatus.runInitializers()`: - It iterates the initializer list **in provider order** and calls each function inside an injection context. - It does **not** await between calls. A returned **Promise** is pushed onto a list; a returned **Observable** is subscribed and wrapped in a Promise that resolves on `complete` and rejects on `error`; a `void` return adds nothing. - After the loop it calls `Promise.all` on the list. When that resolves, initialization is done; if any entry rejects, initialization rejects. - If no initializer returned anything async, initialization completes immediately. The consequence is easy to state and often misremembered: **calls are sequential, waiting is parallel**. | Aspect | Behaviour | |---|---| | Order of calls | Provider order, one synchronous call each | | Waiting between initializers | None | | Waiting before render | All Promises resolved and all Observables completed | | One rejects | Bootstrap fails; the rest are not cancelled but the app never renders | | Total delay | Roughly the slowest initializer, not the sum | ## Why this matters in practice **Hidden dependencies break.** Suppose one initializer loads `/config.json` and a second initializer calls an API whose base URL comes from that config. Both are called in the same loop, so the second starts its request while the first is still in flight and reads an empty config service. Provider order does not fix it, because order only controls when the synchronous part runs. The fixes are: - merge the two steps into one initializer that awaits the config and then makes the call; - or make the second initializer await an explicit Promise exposed by the config service, for example a `ready` Promise. **Parallelism is a feature.** Independent tasks — loading translations, restoring a session, reading feature flags — overlap automatically, so startup costs about as long as the slowest one. Chaining them unnecessarily makes first render slower. **A synchronous throw stops the loop.** If one initializer throws before returning anything, the loop that calls them is interrupted, so initializers listed after it are never called, and bootstrap fails with that error. A rejected Promise behaves differently: every initializer has already been called by then. **Environment initializers are too early for loaded data.** Because they run in step 2, before any application initializer is even called, they must not read values an application initializer loads. **The router can add its own wait.** With the router's blocking initial navigation option, the router registers its own initializer that holds bootstrap until the first navigation reaches a certain point; that belongs to routing, but it explains why startup can wait on guards and resolvers too. ## A mental model to explain it in an interview Think of `runInitializers()` as a manager handing out tasks: it walks down the list and hands each worker a task in order, without waiting for anyone to finish, then waits at the door until the last worker reports back. Only then does the show start. A worker who needs another's output has to be told to wait for it; the manager will not sequence them. ## What a strong answer includes - Calls in provider order, async work concurrent, a single `Promise.all`-style wait. - Observables must complete, not merely emit. - Environment initializers first, root component last. - The fix for dependent initializers: one initializer, or an explicit await.

  • Does moving the config initializer to the top of the providers array guarantee it finishes first?
    No. Provider order only decides which function is called first. Angular does not wait between calls, so a later initializer starts its async work while the earlier one is still pending. Only an explicit await, or merging the steps, creates a real sequence.
  • If one of three initializers rejects, are the other two cancelled?
    No. Their work was already started and keeps running; Angular simply treats initialization as failed once any entry rejects. The root component is never created and the error goes to the ErrorHandler.

A stage manager hands out tasks to the crew in list order without waiting for each to finish, then holds the curtain until the last crew member reports back.

saying these in an interview costs you the question

  • Each initializer waits for the previous one to resolve
  • Provider order guarantees one initializer's data is ready for the next
  • Startup time is the sum of all initializer durations
  • Environment initializers run after application initializers
  • The root component renders while initializers are still pending