📧 Reste informé(e) !

Reçois les derniers articles et conseils EasyAngularKit directement dans ta boîte mail.

S'inscrire gratuitement

~7 min de lecture

Angular 19+ : provideAppInitializer enterre APP_INITIALIZER

Tu ouvres ton app.config.ts pour ajouter un nouvel init bloquant. Tu tombes sur ce monument :

{
  provide: APP_INITIALIZER,
  useFactory: (config: ConfigService, auth: AuthService) => () =>
    config.load().then(() => auth.restoreSession()),
  deps: [ConfigService, AuthService],
  multi: true,
}

Quatre lignes de plomberie pour exprimer une idée d'une ligne : « charge la config, puis restaure la session, et bloque le bootstrap tant que c'est pas fini ». À chaque fois que tu ajoutes une dep, tu dois la rajouter dans deps. Tu oublies, c'est undefined qui arrive, et tu te demandes pourquoi config.load() lève sur un this.http undefined. Le deps: any[] ne te prévient pas, TypeScript regarde ailleurs.

Angular 19 a sorti provideAppInitializer. Et ce n'est pas une alternative cosmétique : l'ancien token est marqué @deprecated dans le source depuis la v19.0.0. Si ton app.config.ts contient encore APP_INITIALIZER, tu maintiens du code officiellement condamné, sans typage. Voilà comment basculer en 4 minutes, et pourquoi ce n'est pas qu'un sucre syntaxique.

Pourquoi APP_INITIALIZER était mal foutu

Le token date de l'époque où Angular vivait sur les modules et les factories. Le DI fonctionnel n'existait pas. inject() non plus. Donc la seule manière d'injecter dans une factory d'initializer, c'était un tableau deps aligné positionnellement sur les params. Quatre conséquences directes :

  • deps non typé. Provider['deps'] est any[]. Ajoute un argument à ta factory, oublie de le déclarer, tu n'as aucun feedback IDE. Le crash arrive à l'exécution, parfois en prod uniquement, parfois SSR uniquement.
  • multi: true obligatoire. Le pattern multi-provider est une convention, pas une garantie typée. Oublie multi: true et tu te prends une erreur runtime cryptique au bootstrap (du genre « Cannot mix multi providers and regular providers »), à toi de deviner que c'est une clé manquante dans un objet provider à l'autre bout du fichier.
  • La double lambda. useFactory ne fournit pas l'initializer, il fournit une fonction qui retourne l'initializer : (config) => () => config.load(). Deux niveaux de fonction, deux occasions de se rater. Retourne la Promise au mauvais niveau ((config) => config.load()) et le bootstrap crashe sur un TypeError obscur. Ajoute des accolades sans return (() => { config.load(); }) et Angular n'attend plus rien : l'app boote sans la config. Le genre de bug qui marche en dev (la config arrive vite) et explose en prod.
  • inject() interdit dans le corps. La factory tournait en contexte d'injection, mais la fonction qu'elle retourne s'exécutait plus tard, hors contexte : un inject() dedans, et c'était NG0203. Donc tout passait par deps, et retour au point 1. (Angular 19 exécute désormais tous les initializers en contexte d'injection, mais sur les versions où APP_INITIALIZER était ta seule option, tu n'avais pas ce luxe.)

L'API était une rustine, et elle a fait son temps.

La solution Angular 19+ : provideAppInitializer

La signature tient en une ligne :

function provideAppInitializer(
  initializerFn: () => Observable<unknown> | Promise<unknown> | void,
): EnvironmentProviders;

Pas de useFactory, pas de deps, pas de multi. Tu passes une fonction. Elle s'exécute dans un contexte d'injection (donc inject() marche directement), et son retour est compris en natif : void (synchrone), Promise, ou Observable. Le runtime attend la résolution avant de booter.

Et côté SSR ? Rien de nouveau à gérer, et c'est une bonne nouvelle : comme avec l'ancien token, le bootstrap (client comme serveur) attend la fin de tes initializers, donc le HTML serveur n'est jamais sérialisé avant que ta config soit prête. Le gain est ailleurs : le contexte d'injection dans le callback, le typage qui remonte, et la fin de la double lambda.

Before / after sur 3 cas réels

Cas 1 : config remote bloquante

// Avant (Angular ≤18)
export const appConfig: ApplicationConfig = {
  providers: [
    {
      provide: APP_INITIALIZER,
      useFactory: (config: ConfigService) => () => config.load(),
      deps: [ConfigService],
      multi: true,
    },
  ],
};
// Après (Angular 19+)
export const appConfig: ApplicationConfig = {
  providers: [
    provideAppInitializer(() => inject(ConfigService).load()),
  ],
};

Quatre clés de provider → un appel. Le typage de ConfigService.load() remonte directement. Pas de deps à maintenir.

Cas 2 : enchaînement config puis auth

L'ancien pattern oblige à séquencer dans la factory. Empiler deux providers ne marche pas : les initializers partent en parallèle, on y revient plus bas.

// Avant
{
  provide: APP_INITIALIZER,
  useFactory: (config: ConfigService, auth: AuthService) => () =>
    config.load().then(() => auth.restoreSession()),
  deps: [ConfigService, AuthService],
  multi: true,
}
// Après
provideAppInitializer(async () => {
  const config = inject(ConfigService);
  const auth = inject(AuthService);
  await config.load();
  await auth.restoreSession();
});

Tu lis le code comme une recette : « inject deux services, attendre la config, attendre l'auth ». Pas besoin d'une ligne de glue. Et si demain tu rajoutes un inject(FeatureFlags), tu n'as rien d'autre à toucher.

Cas 3 : Observable sans wrapper hérité

Soyons précis : APP_INITIALIZER accepte les Observables depuis Angular 12. Mais l'info est passée sous le radar, et une énorme partie des codebases traîne encore des firstValueFrom posés par réflexe (ou hérités d'une époque pré-12 et jamais nettoyés). La migration est le bon moment pour les dégager.

// Avant
{
  provide: APP_INITIALIZER,
  useFactory: (i18n: I18nService) => () => firstValueFrom(i18n.loadLocale$()),
  deps: [I18nService],
  multi: true,
}
// Après
provideAppInitializer(() => inject(I18nService).loadLocale$());

provideAppInitializer accepte l'Observable en natif, et sa signature le rend explicite : il souscrit, attend le complete, propage l'erreur. Si ton Observable est multicast (shareReplay, sujet), pense quand même à le terminer, sinon Angular attend indéfiniment et le bootstrap ne finit pas.

Le piège des erreurs : ton bootstrap meurt en silence

C'est la première chose que les équipes ratent à la migration. Une exception levée dans un initializer stoppe le bootstrap entier. C'était déjà vrai avec APP_INITIALIZER, mais le async/await linéaire de la nouvelle API rend l'oubli plus facile : le code se lit comme un script, et on oublie qu'une ligne qui lève condamne toute l'app à l'écran blanc.

// Mauvais : si load() lève, l'app n'affiche jamais rien.
provideAppInitializer(() => inject(ConfigService).load());

// Bon : tu décides quoi faire d'une config absente.
provideAppInitializer(async () => {
  const config = inject(ConfigService);
  const logger = inject(Logger);
  try {
    await config.load();
  } catch (error) {
    logger.warn('Config load failed, using defaults', error);
  }
});

La règle : un initializer qui peut échouer doit catcher. Sauf si l'app n'a rigoureusement aucun sens sans la donnée (config bloquante volontaire).

L'ordre d'exécution n'est pas séquentiel

Autre piège : tes initializers s'exécutent en parallèle, dans l'ordre où ils sont déclarés mais sans attendre les uns les autres. Si tu as deux provideAppInitializer et que le second a besoin de la config chargée par le premier, tu vas crasher.

// Mauvais : auth.restoreSession() peut partir avant config.load().
providers: [
  provideAppInitializer(() => inject(ConfigService).load()),
  provideAppInitializer(() => inject(AuthService).restoreSession()),
],
// Bon : un seul initializer qui séquence.
providers: [
  provideAppInitializer(async () => {
    await inject(ConfigService).load();
    await inject(AuthService).restoreSession();
  }),
],

Angular attend la résolution de tous les initializers via Promise.all, mais il ne les chaîne pas. Si tu as un ordre logique, garde-le explicite dans un seul callback async.

Le cousin pour les environment injectors : provideEnvironmentInitializer

provideAppInitializer cible le bootstrap racine. Si tu provides un environment injector (par exemple pour une feature lazy ou un sous-arbre de routing), tu veux la même chose mais scopée à cet injecteur. Angular 19 a livré provideEnvironmentInitializer en parallèle.

// Feature lazy avec setup local
export const featureRoutes: Routes = [
  {
    path: 'admin',
    providers: [
      provideEnvironmentInitializer(() => {
        inject(AdminTelemetry).start();
      }),
    ],
    loadChildren: () => import('./admin.routes'),
  },
];

Contrairement à provideAppInitializer, le callback est synchrone uniquement. Le retour est ignoré, et il s'exécute à la création de l'environment injector. Pas de bloquage, pas de Promise attendue. C'est l'équivalent moderne de ENVIRONMENT_INITIALIZER, sans la cérémonie du token.

Migration mécanique en 5 étapes

Si tu veux dégager APP_INITIALIZER de ton repo d'un coup :

  1. grep ton repo pour APP_INITIALIZER. Liste toutes les occurrences.
  2. Pour chaque provider, supprime provide, multi, deps. Garde la fonction interne.
  3. Transforme les params de la factory en appels inject() à l'intérieur du callback.
  4. Si la factory enchaînait plusieurs étapes via .then(), passe en async/await lisible.
  5. Repasse sur les firstValueFrom(...) posés par réflexe autour des Observables : tu peux retourner l'Observable directement.
- {
-   provide: APP_INITIALIZER,
-   useFactory: (a: A, b: B) => () => doStuff(a, b),
-   deps: [A, B],
-   multi: true,
- }
+ provideAppInitializer(() => doStuff(inject(A), inject(B)))

Sur les projets que j'ai migrés, le app.config.ts perd entre 60% et 80% de lignes sur la partie initializers. Et le typage redevient sain.

Récap actionnable

  • APP_INITIALIZER est officiellement @deprecated depuis Angular 19.0. Le source dit « use provideAppInitializer instead ». Ce n'est pas une question de goût, c'est une migration à planifier.
  • provideAppInitializer(fn) remplace les 4 clés provide / useFactory / deps / multi. Le callback s'exécute en contexte d'injection : inject() marche directement, fini la double lambda et le deps: any[].
  • Retour libre : void, Promise<unknown>, Observable<unknown>. Plus de firstValueFrom cosmétique.
  • SSR : rien ne change, et c'est très bien. Le bootstrap serveur attend tes initializers avant de sérialiser le HTML, exactement comme avant.
  • Catch ce qui peut échouer. Une exception non catchée tue le bootstrap entier. C'est voulu, à toi de décider quand c'est désirable.
  • Pas de séquencement implicite. Tes initializers s'exécutent en parallèle. Pour un enchaînement, mets tout dans un seul async callback.
  • provideEnvironmentInitializer pour le scope environnement (feature lazy, sous-arbre de routes). Sync only, pas de bloquage.

Si tu veux un signe concret que ta migration est faite : ouvre ton app.config.ts, et compte combien de fois tu vois encore multi: true. Si la réponse n'est pas zéro, tu n'as pas fini.

📧 Reste informé(e) !

Reçois les derniers articles et conseils EasyAngularKit directement dans ta boîte mail.

S'inscrire gratuitement

AngularKit

Suite d'outils pour développeurs Angular francophones. Apprends, modernise tes réflexes, audite ta codebase.

Produits

Contact

Légal

© 2026 AngularKit. Tous droits réservés.