~9 min de lecture
NG0950 : input.required() lu trop tôt
Tu migres tes @Input() vers les signal inputs. input.required<User>() remplace le @Input() user!: User et son point d'exclamation menteur, le compilateur se met enfin à vérifier que chaque parent binde bien la valeur, le typage est propre. Tu lances l'app. Premier rendu :
NG0950: Input is required but no value is available yet. Find more at https://v22.angular.dev/errors/NG0950
La stack trace pointe vers ton constructor. Ou vers une ligne de déclaration de champ que tu trouvais parfaitement innocente. Et le plus vexant : le parent binde correctement l'input, tu peux le vérifier dans son template. La valeur existe. Elle n'est juste pas encore là au moment où tu la lis.
NG0950 ne t'annonce pas un bug d'Angular : elle te montre la fenêtre morte dans laquelle tu viens de lire. Cet article la délimite, puis en tire trois choses : les endroits où l'erreur tombe, les 4 fenêtres où la lecture est sûre, et le cas voisin qui ne crashe pas mais qui est pire. Tout ce qui suit a été vérifié à l'exécution sur Angular 22.0.7 ; les signal inputs existent depuis Angular 17.1 et sont stables depuis Angular 19.
Ce que dit l'erreur, et ce qu'elle ne dit plus en prod
En dev, le message est explicite : Input is required but no value is available yet. Si tu as posé un alias ou un debugName sur l'input, ce nom-là apparaît dans le message (le debugName gagne si les deux sont posés). En production, ce message vit derrière ngDevMode - le drapeau que ce build supprime - et disparaît du bundle : il te reste NG0950 sec et une stack trace minifiée. C'est le traitement d'une partie des erreurs runtime d'Angular, NG0203 compris.
Sous le capot, c'est trivial. Un input.required() est initialisé avec une valeur sentinelle interne - un marqueur qui veut dire "pas encore écrit" - nommée REQUIRED_UNSET_VALUE. Chaque lecture du signal vérifie si la sentinelle est encore là, et tu peux voir dans la source la précédence du debugName et le message caché derrière ngDevMode :
// @angular/core, reduced to the interesting branch
function inputValueFn() {
producerAccessed(node);
if (node.value === REQUIRED_UNSET_VALUE) {
let message = null;
if (ngDevMode) {
const name = options?.debugName ?? options?.alias;
message = `Input${name ? ` "${name}"` : ''} is required but no value is available yet.`;
}
throw new RuntimeError(-950, message); // NG0950
}
return node.value;
}
Deux conséquences directes, et toute la suite en découle :
- l'erreur tombe à la lecture, pas au binding. Un input required qui n'est jamais lu ne throw pas, même sans binding.
- la question n'est pas "est-ce que le parent fournit la valeur" mais "quand est-ce que je la lis".
La chronologie que tout le monde croit connaître
Sur le papier, tu la connais. Dans l'ordre, pour un composant instancié par un template parent :
- constructor et field initializers - le
= ...posé directement sur un champ de classe - exécutés dans la même phase ; - écriture des inputs par Angular, depuis le code généré du template parent ;
- ngOnInit ;
- rendu du template ;
- afterNextRender et les callbacks post-DOM.
Le point qu'on connaît en théorie et qu'on oublie en pratique : l'étape 1 se déroule avant l'étape 2. Ton composant est construit nu, sans aucun input. C'était déjà vrai avec @Input() : lire this.user dans le constructor donnait undefined depuis toujours. La différence, c'est qu'undefined traversait silencieusement plusieurs couches de code avant d'exploser ailleurs, alors que input.required() crashe sur place avec un code d'erreur documenté. NG0950 ne t'a rien cassé : elle a rendu bruyant un bug que tu écrivais déjà.
Note au passage que le template n'est jamais en cause pour un problème de timing : il est rendu à l'étape 4, après l'écriture des inputs, donc {{ name() }} ne peut pas tomber dans la fenêtre morte de la construction. Ça ne veut pas dire qu'il ne peut jamais throw : si personne n'écrit l'input (on y revient au Piège 3), c'est justement le template qui déclenche NG0950, en tant que premier lecteur. C'est aussi ce qui rend computed sûr, deux sections plus bas.
Piège 1 : la lecture dans le constructor
Le cas le plus direct. Tu veux initialiser quelque chose à partir de l'input :
import { ChangeDetectionStrategy, Component, inject, input } from '@angular/core';
import { AnalyticsService } from './analytics.service';
@Component({
selector: 'app-product-card',
template: `<h2>{{ name() }}</h2>`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ProductCard {
private readonly analytics = inject(AnalyticsService);
readonly name = input.required<string>();
constructor() {
// NG0950: the parent has not written the input yet
this.analytics.trackView(this.name());
}
}
Trois corrections possibles, selon l'intention. Si c'est un "une fois, au démarrage", ngOnInit est la fenêtre prévue pour ça : les inputs sont écrits juste avant.
export class ProductCard implements OnInit {
private readonly analytics = inject(AnalyticsService);
readonly name = input.required<string>();
ngOnInit(): void {
this.analytics.trackView(this.name()); // input available
}
}
Si c'est un "à chaque changement de valeur", c'est un effect. Détail contre-intuitif mais vérifié : tu peux créer l'effect dans le constructor et lire l'input required dans son corps. La création n'exécute rien ; la première exécution est planifiée par Angular après l'écriture des inputs.
export class ProductCard {
private readonly analytics = inject(AnalyticsService);
readonly name = input.required<string>();
constructor() {
// creating the effect here is fine: it runs later, inputs set
effect(() => this.analytics.trackView(this.name()));
}
}
Troisième intention, plus rare : mesurer ou manipuler le DOM rendu à partir de l'input. C'est la fenêtre d'afterNextRender, l'étape 5 de la chronologie.
Piège 2 : le field initializer, la version camouflée
Celui-là passe plus facilement la relecture, parce qu'il ressemble à du code déclaratif :
@Component({
selector: 'app-price-tag',
template: `<span>{{ priceWithVat }}</span>`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PriceTag {
readonly price = input.required<number>();
// runs at construction time, same phase as the constructor: NG0950
readonly priceWithVat = this.price() * 1.2;
}
Un field initializer s'exécute pendant la construction de l'instance, dans la même phase que le constructor. Lire un signal dedans, c'est figer sa valeur du moment ; lire un input required dedans, c'est NG0950. La correction est la même quel que soit le calcul, et elle est meilleure sur tous les plans :
@Component({
selector: 'app-price-tag',
template: `<span>{{ priceWithVat() }}</span>`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PriceTag {
readonly price = input.required<number>();
// lazy AND reactive: evaluated at first read, re-evaluated on change
readonly priceWithVat = computed(() => this.price() * 1.2);
}
computed ne lit rien à la création : il évalue sa fonction à la première lecture - ici le rendu du template, bien après l'écriture des inputs. Et contrairement à la version cassée, il suit les changements de valeur au lieu de photographier la première. Si ton réflexe était "je calcule une fois dans le constructor pour ne pas recalculer", c'est précisément ce que computed mémoïse pour toi, sans figer la valeur au passage.
Piège 3 : partout où le composant naît sans template parent
Côté templates compilés, le compilateur te couvre : un parent qui oublie de binder un input required, c'est l'erreur de compilation NG8008 (Required input 'price' from component PriceTag must be specified.), le build ne passe pas. Mais ce filet s'arrête au template. Tes tests, eux, créent le composant sans template parent :
it('affiche le prix TTC', () => {
const fixture = TestBed.createComponent(PriceTag);
fixture.detectChanges(); // NG0950: nobody ever set `price`
});
Personne n'a écrit price, le template le lit au premier detectChanges, la sentinelle est encore là. La correction : écrire l'input avant le premier cycle de détection, avec setInput.
it('affiche le prix TTC', () => {
const fixture = TestBed.createComponent(PriceTag);
fixture.componentRef.setInput('price', 100);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toContain('120');
});
Les tests ne sont pas seuls dans ce cas. NG8008 ne peut rien vérifier là où aucun template parent n'existe. Mesuré sur trois habitats : les tests, la création dynamique (createComponent(), NgComponentOutlet), et le composant routé. Ce dernier est probablement le plus fréquent dans une vraie application, et tout y dépend de withComponentInputBinding(), l'option de provideRouter() par laquelle le Router écrit les paramètres de route, les query params et le data dans les input() du composant. Deux temps :
- sans
withComponentInputBinding(): personne n'écrit l'input. NG0950 à la première lecture, alors que tout compile. - avec, mais aucune valeur correspondante (ni paramètre de route, ni query param, ni
data) : le Router écrit explicitementundefined, ce qui écrase la sentinelle - comportement par défaut, que l'optionunmatchedInputBehavior: 'undefinedIfStale'(Angular 22) inverse pour retrouver le crash. Pas de NG0950 : ton input required vautundefineden silence, de quoi tempérer le réflexe "dans un composant routé, tout en required". Ce comportement est détaillé dans l'article dédié àwithComponentInputBinding().
Partout ailleurs, dans du template compilé ordinaire, NG0950 signale un problème de timing de lecture, pas de binding.
Le piège silencieux : l'input optionnel a le même bug, sans le crash
Maintenant, la partie qui devrait te faire relire du code existant. Reprends le même bug de timing avec un input optionnel :
export class UserBadge {
readonly plan = input<'free' | 'pro'>('free');
// no crash: frozen on 'free', whatever the parent binds later
private readonly isPro = this.plan() === 'pro';
}
Aucune erreur. Le field initializer lit la valeur par défaut, 'free', y compris quand le parent binde 'pro' un instant plus tard. Ton isPro reste faux pour tous les utilisateurs pro, et rien ne te le dira jamais. Un input<string>() sans défaut fait pareil avec undefined.
C'est le vrai argument pour required : les deux formes ont exactement la même fenêtre morte, mais required la transforme en crash immédiat avec un code documenté, quand l'optionnel te laisse partir en prod avec une valeur périmée. NG0950 est une erreur que tu devrais être content de voir. Si tu la contournes en repassant l'input en optionnel avec un défaut, tu n'as pas corrigé le bug : tu l'as rendu invisible.
La même famille : NG0951 et NG0952
Deux voisines directes, mêmes causes, codes différents :
- NG0951, pour les signal queries required.
viewChild.requiredetcontentChild.requiredsont les versions signal de@ViewChildet@ContentChild; le message estChild query result is required but no value is available.Lire une query required dans le constructor throw, comme pour les inputs. Pour un élément statique du template, la lecture fonctionne dèsngOnInit- vérifié sur Angular 22, la query résout son résultat à la demande dès que la vue est créée. C'est une nuance que l'article dédié aux signal queries ne fait pas encore. Pour une cible dans un@if, un@forou un@switch- créée pendant la passe de rendu, aprèsngOnInit, y compris derrière un@if (true)- il faut attendrengAfterViewInitouafterNextRender; uneffectcréé dans le constructor throw aussi, contrairement à un input. Et une cible dans un@defern'arrive qu'après son déclenchement et le chargement du chunk : là, mêmeafterNextRenderthrow (mesuré avecon immediate). Déclare-la avecviewChild()nullable, pas required. - NG0952, pour
model.required():Model is required but no value is available yet.Même sentinelle, même fenêtre, code dédié - et pas de lien "Find more" dans la console pour celui-là.
Récap : les 4 fenêtres sûres
La règle tient en deux conditions, pas une : quelqu'un écrit l'input, et tu le lis après cette écriture - jamais pendant la construction. Une fois l'écriture garantie, les 4 fenêtres à retenir :
computedpour dériver une valeur : il ne lit rien à la création, sa fonction ne s'évalue qu'à sa première lecture - qui doit elle-même tomber après l'écriture, le plus souvent dans le template (uncomputedlu depuis le constructor crashe pareil). C'est la réponse à la quasi-totalité des cas qui te poussaient vers le constructor.effectpour un effet de bord à chaque changement. Tu peux le créer dans le constructor sans risque : seule son exécution est différée.ngOnInitpour une lecture unique au démarrage : les inputs sont écrits juste avant.afterNextRenderpour du travail sur le DOM rendu.
Et le reste :
- Constructor et field initializers : aucune lecture d'input, required ou pas. Le required crashe (NG0950), l'optionnel te donne une valeur périmée sans prévenir.
- Dans les tests :
fixture.componentRef.setInput('name', value)avant le premierdetectChanges. Un composant à inputs required sanssetInput, c'est NG0950 garanti dès que le template lit la valeur. - NG0950 au runtime : commence par vérifier l'habitat. Test, création dynamique ou route sans
withComponentInputBinding(): cherche qui est censé écrire l'input. Template compilé ordinaire : cherche une lecture trop tôt - le binding manquant dans un template, c'est NG8008 et ça ne compile pas. - Contournement interdit : rendre l'input optionnel pour faire taire NG0950, c'est échanger un crash net contre un état incohérent silencieux.
Prends le crash comme ce qu'il est : un test de timing gratuit, exécuté à la première lecture.