为什么使用 catchError 处理错误而不是在 Angular 的订阅错误回调中处理错误 [英] Why handle errors with catchError and not in the subscribe error callback in Angular

查看:15
本文介绍了为什么使用 catchError 处理错误而不是在 Angular 的订阅错误回调中处理错误的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

所以我通常会像这样写我的 http 请求

服务

getData() {返回 this.http.get('url')}

组件

getTheData() {this.service.getData().subscribe((res) =>{//做一点事},(错误) =>{console.log('getData has throw and error of', err)})

但是查看 Angular 文档,他们似乎在服务中将其格式化为这样

getHeroes(): Observable{返回 this.http.get(this.heroesUrl).管道(catchError(this.handleError('getHeroes', [])));}

这有什么隐含的好处,因为它对我来说似乎很冗长,而且我个人从来没有必要处理我的错误.

解决方案

1 Angular 中的所有关注点分离

使用 catchError 的一个主要好处是将整个数据检索逻辑(包括沿途可能发生的所有错误)与数据的呈现分开.

1.1 让组件只关心数据的呈现

组件应该只关心数据(无论是否存在).他们不应该关心如何检索数据的细节或在数据检索过程中可能出错的所有事情.

<块引用>

组件不应该直接获取或保存数据,它们当然不应故意提供虚假数据.他们应该专注于呈现数据并将数据访问委托给服务.
[Angular 教程 - 为什么选择服务]

假设您的数据是一个项目列表.您的组件将调用 service.getItemList() 函数,并且由于它只关心数据,因此会期望:

  • 包含项目的列表
  • 一个空列表
  • 没有列表,即 nullundefined

您可以使用组件模板中的 ngIf 轻松处理所有这些情况,并根据情况显示数据或其他内容.让 Service 函数返回一个 clean Observable,它只返回数据(或 null)并且不会抛出任何错误,这样可以轻松保持组件中的代码精简使用模板中的 AsyncPipe 进行订阅.

1.2 不要让组件关心数据检索的细节,比如错误

您的数据检索和错误处理逻辑可能会随着时间的推移而改变.也许您正在升级到新的 Api,突然不得不处理不同的错误.不要让你的组件担心这个.将此逻辑移至服务.

<块引用>

从组件中删除数据访问意味着您可以改变对随时实施,无需接触任何组件.他们不了解服务的运作方式.[Angular 教程 - 获取英雄数据]

1.3 将数据检索和错误处理逻辑放在一个服务中

处理错误是数据检索逻辑的一部分,而不是数据呈现逻辑的一部分.

在您的数据检索服务中,您可以使用 catchError 运算符详细处理错误.也许你想对所有错误做一些事情,比如:

  • 记录它
  • 将面向用户的错误消息显示为通知(请参阅显示消息)
  • 获取替代数据或返回默认值

将其中的一些移到 this.handleError('getHeroes', []) 函数中,可以避免重复代码.

<块引用>

向控制台报告错误后,处理程序构造一个用户友好的消息并向应用程序返回一个安全值,以便它可以保留在职的.[Angular 教程 - HTTP 错误处理]

1.4 让未来的开发更简单

有时您可能需要从新组件调用现有服务函数.将错误处理逻辑放在服务函数中可以让这一切变得简单,因为在从新组件调用函数时,您不必担心错误处理.

因此,这归结为将数据检索逻辑(在服务中)与数据呈现逻辑(在组件​​中)分离,并在未来轻松扩展您的应用.

2 保持 Observable 存活

catchError 的另一个用例是在构建更复杂的链式或组合 Observable 时保持 Observable 处于活动状态.在内部 Observable 上使用 catchError 允许您从错误中恢复并保持外部 Observable 运行.当您使用订阅错误处理程序时,这是不可能的.

2.1 链接多个 Observable

看看这个longLivedObservable$:

//永远不会终止/错误const longLivedObservable$ = fromEvent(button, 'click').pipe(switchMap(event => this.getHeroes()));longLivedObservable$.subscribe(console.log);getHeroes(): Observable{返回 this.http.get(this.heroesUrl).pipe(catchError(error => of([])));}

longLivedObservable$ 将在单击按钮时执行 http 请求.即使内部 http 请求抛出错误,它也永远不会终止,因为在这种情况下 catchError 返回一个没有错误但发出空数组的 Observable.

如果您要向 longLivedObservable$.subscribe() 添加错误回调并删除 getHeroes 中的 catchError,则 longLivedObservable$ 会在第一个抛出错误的 http 请求之后终止,并且之后不再对按钮点击做出反应.

<小时>

附注:添加catchError

的Observable很重要

请注意,如果您将 catchErrorgetHeroes 中的内部 Observable 移动到外部,longLivedObservable$ 将终止可观察.

//getHeroes 错误时会终止const longLivedObservable = fromEvent(button, 'click').pipe(switchMap(event => this.getHeroes()),catchError(error => of([])));longLivedObservable.subscribe(console.log);getHeroes(): Observable{返回 this.http.get(this.heroesUrl);}

<块引用>

错误"和完成"通知可能只发生一次Observable Execution,而且只能是其中之一.

在 Observable Execution 中,零到无限的 Next 通知可能是发表.如果发送错误或完成通知,那么之后就不能再交付任何东西了.
[RxJS 文档 - 可观察]

当发送错误(或完成)通知时,Observable 终止.之后他们不能发出任何其他东西.在 Observable 上使用 catchError 不会改变这一点.catchError 不允许你的源 Observable 在发生错误后继续发射,它只是允许你在发生错误时切换到不同的 Observable.此切换仅发生一次,因为只能传递一个错误通知.

在上面的示例中,当 this.getHeroes() 错误时,此错误通知将传播到外部流,导致取消订阅 fromEvent(button, 'click')catchError 切换到 of([]).

在内部 Observable 上放置 catchError 不会将错误通知暴露给外部流.因此,如果您想让外部 Observable 保持活动状态,您必须在内部 Observable 上使用 catchError 处理错误,即直接在它们发生的地方.

<小时>

2.2 组合多个 Observable

当你结合 Observables 时,例如使用 forkJoincombineLatest 您可能希望外部 Observable 在任何内部 Observable 错误时继续.

const animal$ = forkJoin(this.getMonkeys(),this.getGiraffes(),this.getElefants());动物 $.subscribe(console.log);getMonkeys(): Observable{return this.http.get(this.monkeyUrl).pipe(catchError(error => of(null)));}getGiraffes(): Observable{return this.http.get(this.giraffeUrl).pipe(catchError(error => of(null)));}getElefants(): Observable{return this.http.get(this.elefantUrl).pipe(catchError(error => of(null)));}

animals$ 将发出一个包含它可以获取的动物数组的数组,或者在获取动物失败的地方发出 null.例如

[ [ [大猩猩、黑猩猩、倭黑猩猩]、null、[亚洲象、非洲象]]

这里 catchError 允许 animals$ Observable 完成并发出一些东西.

如果您要从所有获取函数中删除 catchError 并改为向 animals$.subscribe() 添加错误回调,则 animals$ 将如果任何内部 Observables 出错,则错误,因此即使某些内部 Observables 成功完成也不会发出任何内容.

要了解更多信息,请阅读:RxJs 错误处理:完整实用指南

So I'd normally write my http requests like this

Service

getData() {
  return this.http.get('url')
}

Component

getTheData() {
  this.service.getData().subscribe(
    (res) => {
      //Do something
    }, 
    (err) => {
      console.log('getData has thrown and error of', err)
    })

But looking through the Angular documentation, they seem to format it like this in a Service

getHeroes(): Observable<Hero[]> {
  return this.http.get<Hero[]>(this.heroesUrl)
    .pipe(
      catchError(this.handleError('getHeroes', []))
    );
}

What's the implicit upside of this as it seems quite verbose to me and I've personally never had the need to pipe my errors.

解决方案

1 It's all about separation of concern in Angular

One major benefit of using catchError is to separate the whole data retrieval logic including all errors that can occur along the way from the presentation of the data.

1.1 Let Components only care about the presentation of data

Components should only care about data (whether it's there or not). They shouldn't care about the specifics of how to retrieve data or all the things that could go wrong during data retrieval.

Components shouldn't fetch or save data directly and they certainly shouldn't knowingly present fake data. They should focus on presenting data and delegate data access to a service.
[Angular Tutorial - Why Services]

Let's say your data is a list of items. Your Component would call a service.getItemList() function and, as it only cares about data, would expect:

  • a list containing items
  • an empty list
  • no list i.e. null or undefined

You could easily handle all these cases with ngIf in your Component template and display the data or something else depending on the case. Having a Service function return a clean Observable that only returns data (or null) and isn't expected to throw any errors keeps the code in your Components lean as you can easily use the AsyncPipe in a template to subscribe.

1.2 Don't let Components care about data retrieval specifics like errors

Your data retrieval and error handling logic may change over time. Maybe you're upgrading to a new Api and suddenly have to handle different errors. Don't let your Components worry about that. Move this logic to a Service.

Removing data access from components means you can change your mind about the implementation anytime, without touching any components. They don't know how the service works. [Angular Tutorial - Get hero data]

1.3 Put the data retrieval and error handling logic in a Service

Handling errors is part of your data retrieval logic and not part of your data presentation logic.

In your data retrieval Service you can handle the error in detail with the catchError operator. Maybe there are some things you want to do on all errors like:

  • log it
  • display a user oriented error message as a notification (see Show messages)
  • fetch alternative data or return a default value

Moving some of this into a this.handleError('getHeroes', []) function keeps you from having duplicate code.

After reporting the error to console, the handler constructs a user friendly message and returns a safe value to the app so it can keep working. [Angular Tutorial - HTTP Error handling]

1.4 Make future development easier

There may come a time when you need to call an existing Service function from a new Component. Having your error handling logic in a Service function makes this easy as you won't have to worry about error handling when calling the function from your new Component.

So it comes down to separating your data retrieval logic (in Services) from your data presentation logic (in Components) and the ease of extending your app in the future.

2 Keeping Observables alive

Another use case of catchError is to keep Observables alive when you're constructing more complex chained or combined Observables. Using catchError on inner Observables allows you to recover from errors and keep the outer Observable running. This isn't possible when you're using the subscribe error handler.

2.1 Chaining multiple Observables

Take a look at this longLivedObservable$:

// will never terminate / error
const longLivedObservable$ = fromEvent(button, 'click').pipe(
  switchMap(event => this.getHeroes())
);
longLivedObservable$.subscribe(console.log);

getHeroes(): Observable<Hero[]> {
  return this.http.get<Hero[]>(this.heroesUrl).pipe(
    catchError(error => of([]))
  );
}

The longLivedObservable$ will execute a http request whenever a button is clicked. It will never terminate not even when the inner http request throws an error as in this case catchError returns an Observable that doesn't error but emits an empty array instead.

If you would add an error callback to longLivedObservable$.subscribe() and removed catchError in getHeroes the longLivedObservable$ would instead terminate after the first http request that throws an error and never react to button clicks again afterwards.


Excursus: It matters to which Observable you add catchError

Note that longLivedObservable$ will terminate if you move catchError from the inner Observable in getHeroes to the outer Observable.

// will terminate when getHeroes errors
const longLivedObservable = fromEvent(button, 'click').pipe(
  switchMap(event => this.getHeroes()),
  catchError(error => of([]))
);
longLivedObservable.subscribe(console.log); 

getHeroes(): Observable<Hero[]> {
  return this.http.get<Hero[]>(this.heroesUrl);
}

"Error" and "Complete" notifications may happen only once during the Observable Execution, and there can only be either one of them.

In an Observable Execution, zero to infinite Next notifications may be delivered. If either an Error or Complete notification is delivered, then nothing else can be delivered afterwards.
[RxJS Documentation - Observable]

Observables terminate when an Error (or Complete) notification is delivered. They can't emit anything else afterwards. Using catchError on an Observable doesn't change this. catchError doesn't allow your source Observable to keep emitting after an error occurred, it just allows you to switch to a different Observable when an error occurs. This switch only happens once as only one error notification can be delivered.

In the example above, when this.getHeroes() errors this error notification is propagated to the outer stream leading to an unsubscribe from fromEvent(button, 'click') and catchError switching to of([]).

Placing catchError on the inner Observable doesn't expose the error notification to the outer stream. So if you want to keep the outer Observable alive you have to handle errors with catchError on the inner Observable, i.e. directly where they occur.


2.2 Combining multiple Observables

When you're combining Observables e.g. using forkJoin or combineLatest you might want the outer Observable to continue if any inner Observable errors.

const animals$ = forkJoin(
  this.getMonkeys(), 
  this.getGiraffes(), 
  this.getElefants()
);
animals$.subscribe(console.log);

getMonkeys(): Observable<Monkey[]> {
  return this.http.get<Monkey[]>(this.monkeyUrl).pipe(catchError(error => of(null)));
}

getGiraffes(): Observable<Giraffe[]> {
  return this.http.get<Giraffe[]>(this.giraffeUrl).pipe(catchError(error => of(null)));
}

getElefants(): Observable<Elefant[]> {
  return this.http.get<Elefant[]>(this.elefantUrl).pipe(catchError(error => of(null)));
}

animals$ will emit an array containing the animal arrays it could fetch or null where fetching animals failed. e.g.

[ [ Gorilla, Chimpanzee, Bonobo ], null, [ Asian Elefant, African Elefant ] ]

Here catchError allows the animals$ Observable to complete and emit something.

If you would remove catchError from all fetch functions and instead added an error callback to animals$.subscribe() then animals$ would error if any of the inner Observables errors and thus not emit anything even if some inner Observables completed successfully.

To learn more read: RxJs Error Handling: Complete Practical Guide

这篇关于为什么使用 catchError 处理错误而不是在 Angular 的订阅错误回调中处理错误的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆