Cette erreur se produit lorsqu'une resource reposant sur un Observable — rxResource ou httpResource — se termine (complete) sans jamais avoir émis de valeur ni d'erreur.
Une resource doit toujours aboutir à quelque chose : une valeur qu'elle a chargée, ou une erreur expliquant ce qui s'est mal passé. Un Observable qui se termine silencieusement ne fournit ni l'un ni l'autre à Angular : Angular ne sait donc pas quoi mettre dans la resource, et lève cette erreur à la place.
Avec httpResource
httpResource ne vous fait pas écrire d'Observable vous-même : il effectue l'appel HTTP en interne via HttpClient et se termine dès que la réponse arrive. Si cette erreur provient de httpResource, la requête elle-même n'est donc pas en cause : quelque chose placé devant elle absorbe la réponse avant qu'elle n'arrive.
Le suspect habituel est un HttpInterceptor qui intercepte une erreur sans rien réémettre : la même erreur avec EMPTY que ci-dessous, mais dans un intercepteur plutôt que dans votre propre code :
@Injectable()
class MyInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<unknown>, next: HttpHandler) {
return next.handle(req).pipe(
// BAD: same problem as `catchError(() => EMPTY)` in a `stream` —
// the request completes with no response and no error, and any
// httpResource depending on it hits NG0991.
catchError(() => EMPTY),
);
}
}
Corrigez-le comme vous le feriez de manière générale dans un pipeline d'intercepteurs : laissez l'erreur se propager (supprimez le catchError), ou remplacez-le par quelque chose qui résout réellement la requête, comme of(new HttpResponse({body: fallbackValue})).
Avec rxResource
La façon la plus courante de provoquer cette erreur par accident est d'intercepter une erreur et de l'absorber avec EMPTY :
import {rxResource} from '@angular/core/rxjs-interop';
import {EMPTY, catchError} from 'rxjs';
const userResource = rxResource({
params: () => ({id: userId()}),
stream: ({params}) =>
this.http.get(`/users/${params.id}`).pipe(
// BAD: this hides the error completely. The Observable just
// completes with nothing, and the resource has no value and no
// error to show for it — this is what triggers NG0991.
catchError(() => EMPTY),
),
});
Autre cas fréquent : combiner plusieurs Observables (avec merge, race et équivalents) dont l'un peut se terminer de lui-même avant d'avoir émis, sans startWith(...) pour garantir une valeur initiale. Si aucune des sources combinées n'émet avant que toutes se soient terminées, le flux combiné se termine lui aussi à vide.
Choisissez la correction adaptée à votre cas :
Laissez passer l'erreur. Si vous n'avez pas besoin de gérer l'erreur vous-même, supprimez simplement le catchError : la resource se retrouvera dans son état error, que vous pouvez déjà vérifier avec userResource.error().
stream: ({params}) => this.http.get(`/users/${params.id}`),
Rattrapez l'erreur avec une valeur de repli. Si vous préférez afficher quelque chose plutôt qu'une erreur, utilisez catchError pour renvoyer un Observable qui émet une valeur, comme of(...) :
import {of, catchError} from 'rxjs';
stream: ({params}) =>
this.http.get(`/users/${params.id}`).pipe(
catchError(() => of(null)), // resource resolves with `null` instead of erroring
),
Dans les deux cas, assurez-vous que l'Observable renvoyé par stream émet toujours au moins une valeur ou une erreur avant de se terminer.
Où cette erreur apparaît réellement
L'erreur n'est pas levée à la création de la resource, ni nécessairement à l'endroit où la requête se termine. Elle est levée la prochaine fois que quelque chose lit la valeur de la resource, par exemple userResource.value() dans un binding de template. Le bug peut alors sembler se trouver ailleurs dans votre application, alors que la vraie cause est en amont (le stream qui se termine à vide, ou un intercepteur qui absorbe la réponse).
Si rien dans votre code ne vérifie le statut de la resource avant de lire .value(), cette erreur peut remonter jusqu'au ErrorHandler global d'Angular, comme toute autre erreur non interceptée levée pendant le rendu d'un template.
Deux façons d'en réduire l'impact, de la plus à la moins recommandée :
- Corrigez le problème à la racine, avec l'une des approches ci-dessus, pour que la resource ne se retrouve jamais dans cet état.
- Protégez les lectures de
.value()par mesure de défense en profondeur, au cas où quelque chose que vous ne maîtrisez pas entièrement (un intercepteur ou un service tiers, par exemple) se terminerait un jour à vide de manière inattendue. Vérifiez.hasValue()ou.status()avant de lire.value(), par exemple avec@if (userResource.hasValue()) { {{ userResource.value() }} }, au lieu de lire.value()sans condition.