Dans un projet frontend, lorsqu'on fait un appel HTTP, il ne faut pas oublier la gestion des cas d'erreurs. Un appel HTTP peut être en erreur pour diverses raisons, on peut citer :
- un serveur est inaccessible : le backend est "tombé" à cause d'une erreur interne par exemple
- un timeout si la requête prend plus d'un certain temps à répondre
- une erreur renvoyée par le backend avec un message spécifique : l'utilisateur n'a pas le droit d'accéder à cette ressource par exemple
Dans chaque cas, si le frontend ne gère pas ces erreurs, on se retrouve avec une application qui fonctionne mal ou, dans le pire des cas, plus du tout.
Dans cet article, je vais vous présenter la façon de gérer vos erreurs lors d'un appel HTTP pour un projet Angular. On verra d'abord la gestion des erreurs dans un subscribe, puis la gestion des erreurs dans un effect.
Prenons l'exemple d'un service HobbitsService et de la méthode findHobbits qui fait un appel HTTP pour retourner un observable d'une liste d'Hobbits.
@Injectable()
export class HobbitsService {
constructor(private http: HttpClient) {}
findHobbits(): Observable<Hobbit[]> {
return this.http.get<Hobbit[]>('api/hobbits');
}
}
On veut afficher la liste des Hobbits, et pendant que la requête HTTP est en cours, on affiche un loader à l'utilisateur.
Gérer les erreurs dans un subscribe
Exemple d'une erreur non traitée
Dans le composant HobbitsComponent une liste d'Hobbits est récupérée à l'initialisation du composant. Un loader est affiché lorsque le booléen isLoading est à true.
export class HobbitsComponent implements OnInit {
isLoading = true;
hobbits: Hobbit[] = [];
constructor(private hobbitsService: HobbitsService) {}
ngOnInit() {
this.hobbitsService.findHobbits().subscribe(
(hobbits: Hobbit[]) => {
this.hobbits = hobbits;
this.isLoading = false;
}
);
}
}
Que se passe-t-il si l'appel
findHobbitséchoue ?
Le loader va être affiché, sans s'arrêter, alors que l'appel est terminé.
Pourquoi ?
La gestion du statut du loader est placé dans la fonction NEXT du subscribe. Quand une erreur survient, on ne passe pas dans NEXT mais dans la fonction ERROR du subscribe.
NEXT, ERROR, COMPLETE : les 3 fonctions d'un subscribe
subscribe a 3 fonctions optionnelles : NEXT, ERROR, COMPLETE.
this.hobbitsService.findHobbits().subscribe(
() => console.log('Next'),
() => console.log('Error'),
() => console.log('Completed')
);
Si l'appel HTTP réussit, on voit les logs suivant :
Next
Completed
En cas de succès, la valeur est émise dans la fonction NEXT. Puis l'observable se ferme et il passe dans la fonction COMPLETE. C'est la fin du lifecycle de l'observable, aucune erreur n'a été émise.
Si l'appel HTTP échoue, on voit les logs suivant :
Error
En cas d'erreur, aucune valeur n'est émise dans la fonction NEXT. On passe dans la fonction ERROR, c'est la fin du lifecycle de l'observable.
A savoir :
- Un appel HTTP est un observable qui "complete" après avoir émit une valeur. On a alors deux "chemins" possibles :
- On ne peut pas être dans un
COMPLETEetERRORdans le lifecycle d'un observable, c'est soit l'un, soit l'autre.
Pour résoudre le problème
Pour gérer l'affichage du loader en cas d'erreur, on va traiter son état dans la fonction NEXT et dans la fonction ERROR.
export class HobbitsComponent implements OnInit {
isLoading = true;
hobbits: Hobbit[] = [];
constructor(private hobbitsService: HobbitsService) {}
ngOnInit() {
this.hobbitsService.findHobbits().subscribe(
(hobbits: Hobbit[]) => {
this.hobbits = hobbits;
this.isLoading = false;
},
() => {
this.isLoading = false;
}
);
}
}
Si l'appel HTTP réussit ou échoue, on aura le booléen isLoading à false et donc on n'aura plus le loader affiché à l'infini.
Traiter ou logger l'erreur
Dans le cas où on veut utiliser l'erreur pour debugger ou pour afficher un message précis à l'utilisateur par exemple, on peut utiliser l'erreur retournée comme ceci :
this.hobbitsService.findHobbits().subscribe(
() => console.log('Next'),
(error) => console.log('Error', error),
() => console.log('Completed')
);
Gestion les erreurs dans un effect
Pour gérer vos effets de bord, par exemple vos appels backends, vous pouvez également utiliser la librarie NGRX et les effects. Personnellement c'est la manière dont je gère ces appels. Je ne donne pas la responsabilité au composant de récupérer les données.
L'action loadHobbits met un booléen isLoading à true dans le store. L'action loadHobbitsSuccess passe ce booléen à false et enregistre la liste des Hobbits dans le store. Le loader est affiché si le booléen isLoading est à true
Exemple sans gestion d'erreur
@Injectable()
export class HobbitsEffects {
loadHobbits$ = createEffect(() =>
this.actions$.pipe(
ofType(loadHobbits),
concatMap(() =>
this.hobbitsService.findHobbits().pipe(
map((hobbits: Hobbit[]) => loadHobbitsSuccess({ hobbits }))
)
)
)
);
constructor(
private actions$: Actions,
private hobbitsService: HobbitsService
) {}
}
Que se passe-t-il si l'appel
findHobbitséchoue ?
Le loader va être affiché, sans s'arrêter, alors que l'appel est terminé.
Pourquoi ?
Seul l'action loadHobbitsSuccess met le booléen isLoading à false. Or, en cas d'erreur, on ne passe pas dans le map qui suit l'appel HTTP. Il faudrait attraper l'erreur à l'aide de l'opérateur catchError.
catchError
L'opérateur catchError va permettre, comme son nom l'indique, d'attraper l'erreur et de retourner un nouvel observable.
this.hobbitsService.findHobbits().pipe(
map(() => /*SUCCESS*/),
catchError(() => of(/*ERROR*/)),
);
Pour résoudre le problème
On va créer une nouvelle action loadHobbitsError qui va permettre dans notre exemple de mettre le booléen isLoading à false et donc d'arrêter d'afficher le loader en cas d'erreur.
@Injectable()
export class HobbitsEffects {
loadHobbits$ = createEffect(() =>
this.actions$.pipe(
ofType(loadHobbits),
concatMap(() =>
this.hobbitsService.findHobbits().pipe(
map((hobbits: Hobbit[]) => loadHobbitsSuccess({ hobbits })),
catchError(() => of(loadHobbitsError()))
)
)
)
);
constructor(
private actions$: Actions,
private hobbitsService: HobbitsService
) {}
}
A savoir :
- Si vous êtes sur une version antérieure à la version 8 d'
NGRX, en cas d'erreur "non attrapée" dans l'observable principal à l'aide d'uncatchError, l'effectestcomplete. Depuis la version 8, si aucune erreur est "attrapée" dans l'observable principal, l'effectse resouscrit avec une limite maximum d'erreurs.
Appels multiples
En cas d'appels multiples, on peut choisir de retourner un observable avec des données pour gérer les cas d'appels qui ont échoués.
Dans l'exemple ci-dessous, on a une liste d'ids d'Hobbits donnée par l'action loadHobbitsBeers.
Pour chaque id d'Hobbit, on fait un appel HTTP via favoriteBeersByHobbitId qui va retourner une liste de string qui correspond aux bières préférées d'un Hobbit donné.
Ces appels sont effectués en parallèles, et si l'un d'eux échoue, on enregistre l'id du Hobbit, ainsi que la bière Prancing Pony's Ale par défaut. Ainsi, les appels qui ont échoué sont traités avec des données par défaut.
@Injectable()
export class HobbitsEffects {
loadHobbitsDetails$ = createEffect(() =>
this.actions$.pipe(
ofType(loadHobbitsBeers),
mergeMap(({ hobbitsIds }) =>
forkJoin(
hobbitsIds.map(hobbitId =>
this.hobbitsService.favoriteBeersByHobbitId(hobbitId).pipe(
map((beers: string[]) => ({
id: hobbitId,
beers,
})),
catchError(() =>
of({
id: hobbitId,
beers: [`Prancing Pony's Ale`]
})
)
)
)
)
),
map((hobbitsBeers: HobbitsBeers[]) => loadHobbitsBeersSuccess({ hobbitsBeers }))
)
);
constructor(
private actions$: Actions,
private hobbitsService: HobbitsService
) {}
}
Traiter ou logger l'erreur
Dans le cas où on veut utiliser l'erreur pour debugger ou pour afficher un message précis à l'utilisateur par exemple, on peut utiliser l'erreur retournée comme ceci :
this.hobbitsService.findHobbits().pipe(
map((hobbits: Hobbit[]) => /*SUCCESS*/),
catchError((error) => {
console.log('ERROR', error);
return of(/*ERROR*/);
})
)

Top comments (0)