抛出错误_RxJS 错误处理 - 完整实用指南

cc5b5c4b8a5990bef4902a3de775b4bd.png

最近在解决一个微前端的缺陷,在某种场景下刷新页面跨应用渲染组件会报一个组件未注册的错误,这个错误本身无关紧要,但是一旦错误抛出后控制台会打印两条错误日志(map 操作处理函数会执行2次,整个 load 函数内部通过 shareReplay 后自己订阅了一次,外部调用 load 函数时也订阅了一次),好奇心驱使我要好好研究一下 RxJS 的错误处理机制。

c0182d0904e0d9bcdfb575e306522122.png

最终发现一篇英文博客写的特别好:RxJs Error Handling: Complete Practical Guide

所以我就使用我那极不专业的英文翻译了一下,翻译的不好勿见怪,英文好的直接看原文吧,以下是翻译内容:

错误处理是 RxJS 的一个特别重要的部分,因为我们编写响应式代码的任何时候都需要处理错误。

RxJS 中的错误处理非常难以理解,稍不注意就会踩坑,但是如果我们对于 Observable 契约的理解非常深刻,那么理解错误处理就很简单了。

在这篇文章中,我们将提供一个完整的指南,其中包含最常见的错误处理策略,这些策略将覆盖涵盖大多数实际的场景,首先让我们从基础(Observable 对象的契约)开始。

目录

  • Observable 契约和错误处理
  • RxJS 订阅和错误回调函数
  • catchError 操作符
  • 捕获和替换策略
  • 捕获和再抛出策略
  • 在 Observable 链中多次使用 catchError
  • Finalize 操作符
  • 重试策略
  • retryWhen 操作符
  • 创建一个通知流
  • 立即重试策略
  • 延迟重试策略
  • delayWhen 操作符
  • 创建 Observable 的函数 timer
  • Github 仓储 (代码示例)
  • 结束

Observable 契约和错误处理

为了理解 RxJS 中的错误处理,我们需要首先理解:任何给定的流只能出错一次。这是由 Observable 契约定义的,它表示流可以发出零个或多个值,Observable 契约这样工作是因为我们在运行时观察到的所有流的都是如此,例如:网络请求可能会失败。

一个 Stream 完成了意味着:

  • Stream 已结束其生命周期,没有报任何错误
  • 完成后,Stream 将不再发出任何其他值

还有一种完成就是报错了,报错意味着:

  • Stream 带着错误结束了生命周期
  • 当错误抛出后,Stream 将不再发出任何其他值

需要注意的是:Stream 的完成和错误是互斥的

  • 如果一个 Stream 已经完成(complete)了,它不可能在之后报错
  • 如果一个 Stream 报错了,它不可能在之后完成

还有就是 Stream 完成和出错是可选的,两种情况只能发生一种,不能同时发生,当一个特定的 Stream 出错时,我们就不能再使用它了,根据 Observable 的契约,你一定在想,我们怎么才能从错误中把 Stream 恢复过来呢?

RxJS 订阅和错误回调函数

当我们在 RxJS 中创建一个 Stream 并通过 subscribe 函数订阅它,subscribe 函数有三个可选参数:

  • 成功处理函数 next,每次流发出值时调用该函数
  • 错误处理函数 error,只有在发生错误时才调用该函数,此处理函数本身接收一个错误
  • 完成处理函数 complete,仅当流完成时才调用该函数
@Component({
    selector: 'home',
    templateUrl: './home.component.html'
})
export class HomeComponent implements OnInit {

    constructor(private http: HttpClient) {}

    ngOnInit() {

        const http$ = this.http.get<Course[]>('/api/courses'); 

        http$.subscribe(
            res => console.log('HTTP response', res),
            err => console.log('HTTP Error', err),
            () => console.log('HTTP request completed.')
        );
    }
}

流的 Completion 行为

如果上述的流没有报错,它将会在控制台打印如下日志:

HTTP response {payload: Array(9)}
HTTP request completed.

我们可以看出, HTTP 的流只会 Emit 一个值,发出后完成了它,意味着没有错误发生。

但是如果 HTTP 请求报错,那么控制台只会打印一个错误的日志,并不会 Emit 任何值,同时也不会完成。

3b227cb43ea0081ecc61a781d947043b.png

订阅错误处理的限制

有时候我们通过 subscribe 订阅来处理错误,但是这种处理方式是有限制的。

比如我们不能恢复错误或者 Emit 一个 回退值(fallback)替换原本期望服务端应该返回的值,接下来我们学习一些操作符来实现一些错误处理的高级策略。

catchError 操作符

在同步编程中我们可以通过 try {} catch {}块包裹来捕获任何错误,然后在 catch 中处理错误:

try {
   // synchronous operation
   const httpResponse =  getHttpResponseSync('/api/courses');
}
catch(error) {
    // handle error
}

这样处理错误非常简单,但是 JavaScript 中大部分操作都是异步的,比如请求一个远程 API, RxJS 中提供的 catchError 操作符帮助我们处理类似的场景。(备注:不叫 catch 是因为 catch 是关键字)

catchError 如何工作的?

与其他操作符一样,catchError 只是一个函数,接受一个 Observable,输出一个 Observable。每次调用catchError时需要传入一个错误处理函数。

catchError 操作符将一个可能出错的 Observable 作为输入,并发出和输入一样的 Observable,如果没有错误,catchError 产生的输出 Observable 与输入 Observable 完全相同。

当错误抛出会发生什么?

但是,如果发生了错误,那么catchError逻辑将起作用catchError运算符将接收错误并将其传递给错误处理函数。错误处理函数将返回一个 Observable,它将替代刚刚出错的流并返回给下游。

让我们记住catchError的输入流已经出错,因此根据 Observable 的契约,我们不能再使用它了, 这个替换的 Observable 随后将被订阅,它的值将被用来代替出错的输入 Observable。

捕获和替换策略

让我们举一个例子,说明如何使用 catchError 来提供发出回退值的替换 Observable:

const http$ = this.http.get<Course[]>('/api/courses');

http$
    .pipe(
        catchError(err => of([]))
    )
    .subscribe(
        res => console.log('HTTP response', res),
        err => console.log('HTTP Error', err),
        () => console.log('HTTP request completed.')
    ); 

让我们来分解一下捕获和替换策略:

  • 我们传递给 catchError 一个错误处理函数
  • 错误处理函数不会立即调用,通常也不会调用
  • 只有当 catchError 的 Observable 输入出现错误时,才会调用错误处理函数
  • 如果 Observable 输入流中发生错误,则此函数将返回使用 of([])函数构建的 Observable 对象
  • of() 函数生成一个 Observable 并发出一个值 ([]) 同时完成了这个流
  • 错误处理函数返回了一个恢复的流of([]),并且被 catchError 操作符订阅
  • 恢复流的值发出值替换了输出的流,被 catchError 返回

最终结果就是 http$ Observable 将不再报错,我们将会再控制台得到如下结果:

HTTP response []
HTTP request completed.

我们将看到 subscribe 的错误处理函数将不再调用:

  • 空的数组[]被发送出去
  • http$ Observable 完成

尽管原来的 Observable 出错了,替换的 Observable 被用来为 http$ 的订阅者提供一个默认的回退值[]

注意:在返回替换的 Observable 之前,我们还可以添加一些本地错误处理,比如记录错误日志等操作。

以上是捕获和替换策略,接下来让我们看看如何使用 catchError 来重新抛出错误,而不是提供回退值。

捕获和再抛出策略

我们首先注意,替换策略中的 catchError 本身也会出错,一旦发生错误,错误将会传播到 catchError 的输出 Observable,这种错误传播行为为我们提供了一种机制,让我们可以在本地处理错误后重新抛出 catchError 捕获的错误:

const http$ = this.http.get<Course[]>('/api/courses');

http$
    .pipe(
        catchError(error => {
            console.log('Handling error locally and rethrowing it...', error);
            return throwError(error);
        })
    )
    .subscribe(
        res => console.log('HTTP response', res),
        error => console.log('HTTP Error', error),
        () => console.log('HTTP request completed.')
    );

捕捉并重新抛出故障

让我们一步一步地分解捕获和再抛出策略:

  • 像之前一样,我们捕获了错误并返回了一个替代的 Observable
  • 但是这一次,我们并没有返回一个回退值 [] 作为输出,我们在 catchError 中本地处理了错误
  • 在本例中,我们只是简单的记录错误到控制台,但是我们可以添加任何我们想要的错误处理逻辑,比如向用户展示错误信息
  • 然后我们返回一个替换的 Observable,这次是使用 throwError 创建的
  • throwError 创建了一个从不发出任何值的可观察对象,它会立即抛出一个 catchError 捕捉的错误
  • 这意味着 catchError 的 Observable 输出也将与 catchError 的输入所抛出的完全相同的错误一起出错
  • 这意味着我们成功地将 catchError 的 input Observable 抛出的错误重新抛出到输出 Observable
  • 如果需要的话,这个错误可以被接下来的 Observable 链上处理

如果我们运行代码,将会在控制台输出错误日志

a7d6f55789fa256e275b910e5c0392ef.png

我们看到,同一个错误既被记录在 catchError 块中,也会出现在 subscribe 错误处理函数中。

在 Observable 链中多次使用 catchError

请注意,如果需要,我们可以在 Observable 链的不同点多次使用 catchError,并在链的每个点采用不同的错误策略。

例如:我们可以在 Observable 链中捕捉到一个错误,在本地处理并重新抛出,然后在 Observable 链的下一步,我们可以再次捕获相同的错误,这一次我们可以提供一个回退值(而不是重新抛出)。

const http$ = this.http.get<Course[]>('/api/courses');

http$
    .pipe(
        map(res => res['payload']),
        catchError(err => {
            console.log('caught mapping error and rethrowing', err);
            return throwError(err);
        }),
        catchError(err => {
            console.log('caught rethrown error, providing fallback value');
            return of([]);
        })
    )
    .subscribe(
        res => console.log('HTTP response', res),
        err => console.log('HTTP Error', err),
        () => console.log('HTTP request completed.')
    );

运行代码将会在控制台输出:

6066564e42a5e9a97cfe90d36d1243bb.png

该错误最初确实是重新抛出了,但它从未到达 subscribe 错误处理,因为回退值 [] 按预期发出。

Finalize 操作符

JavaScript 中的try {} catch 捕获同步代码错误的时候提供了 finally 关键字,表示无论如何总会执行的代码块。finally 代码块一般用于释放昂贵的资源,比如:关闭网络连接或者释放内存。与 catch 块中的代码不同,无论是否抛出错误,finally 块中的代码将独立执行。

try {
   // synchronous operation
   const httpResponse =  getHttpResponseSync('/api/courses');
}
catch(error) {
    // handle error, only executed in case of error
}
finally {
    // this will always get executed
}

RxJS 为我们提供了 finalize 操作符实现类似的功能(备注:不叫 finally 是因为 finally 在 JavaScript 中是关键字)

Finalize 操作符示例

catchError 操作符一样,我们可以在 Observable 链的不同位置添加多个 finalize 调用,以确保正确释放多个资源:

const http$ = this.http.get<Course[]>('/api/courses');

http$
    .pipe(
        map(res => res['payload']),
        catchError(err => {
            console.log('caught mapping error and rethrowing', err);
            return throwError(err);
        }),
        finalize(() => console.log("first finalize() block executed")),
        catchError(err => {
            console.log('caught rethrown error, providing fallback value');
            return of([]);
        }),
        finalize(() => console.log("second finalize() block executed"))
    )
    .subscribe(
        res => console.log('HTTP response', res),
        err => console.log('HTTP Error', err),
        () => console.log('HTTP request completed.')
    );

运行这段代码,看看多个 finalize 块是如何执行的:

2c2087eba0a331bbe0eb901895e36933.png

注意:最后一个 finalize 块是在 subscribe value 处理函数和 completion 处理函数之后执行的。

重试策略

对于错误处理,我们上面介绍了 catchError 返回回退值,或者重新报错的方法,我们还可以简单地重试订阅出错的 Observable。

之前我们一直在强调:一旦流出错,我们就无法恢复它,但是我们可以再次订阅流的来源,并创建另一个流:

  • 我们获取输入流并且订阅它,创建一个新的流
  • 如果流没有出错,我们将使用它的返回值显示在输出流中
  • 如果流出错了,我们重新订阅一次输入,创建一个新的流

什么时候使用 retry

最大的问题是:我们什么时候才能再次订阅 Observable 的输入,然后重试执行输入流?

  • 我们需要立即重试吗?
  • 我们需要等待一个小的延迟,希望问题解决后再次重试?
  • 我们的重试次数是否有一个限制,当超出限制后再次报错

为了更好的回答这个问题,我们需要第二个辅助的 Observable,我们可以叫它通知流,这个通知流决定何时重试,通知流可以使用 retryWhen 操作符,它是重试策略的核心。

RxJS retryWhen 操作符弹珠图

9960d58bbd713ee754384e4682d0d1ef.png

注意开始重试是在第二行的 1-2 Observable 顶部,而不是第一行的 Observable。

第一行的 Observable 发出 r-r 值得流是通知流,它将决定何时重试。

分解 retryWhen 的工作原理

  • Observable 1-2 流被订阅,它的值立即反应在 retryWhen 返回流的输出
  • 即使 Observable 1-2 流已经完成,但是它仍然可以被重试
  • 通知流在 Observable 1-2 流完成后发出 r 值
  • 通知流可以发出任何值,在此示例中发出了 r
  • 重要的是 r 值发出的时刻,将触发 1-2 Observable 被重试
  • Observable 1-2 将会被 retryWhen 再次订阅,它的值反应在 retryWhen 的输出 Observable 中
  • 通知 Observable 继续发出另一个 r 值,同样会触发 1-2 Observable 被重试
  • 但是此时通知 Observable 完成了
  • 此时,1-2 Observable 正在进行的重试尝试也提前完成,所以只发出了值1,值2还未发出

retryWhen 在每次通知 Observable 发出一个值时都会简单地重试输入 Observable,现在我们理解了 retryWhen 的工作机制,让我们开始创建一个通知流。

创建一个通知流

我们需要在传递给 retryWhen 操作符的函数中直接创建通知 Observable,此函数将一个可观察的错误作为输入参数,它将发出可观察输入的错误作为值。

因此,通过订阅这些 Observable 的错误,我们可以准确地知道错误何时发生,现在让我们看看如何使用Observable 的错误来实现立即重试策略。

立即重试策略

为了在错误发生后立即重试失败的 Observable,我们所要做的就是返回 Observable 的错误,而不做任何事情,在本例中,我们只是将 tap 操作符用于日志记录,因此 Observable 的错误保持不变:

const http$ = this.http.get<Course[]>('/api/courses');

http$.pipe(
        tap(() => console.log("HTTP request executed")),
        map(res => Object.values(res["payload"]) ),
        shareReplay(),
        retryWhen(errors => {
            return errors
                    .pipe(
                        tap(() => console.log('retrying...'))
                    );
        } )
    )
    .subscribe(
        res => console.log('HTTP response', res),
        err => console.log('HTTP Error', err),
        () => console.log('HTTP request completed.')
    );

让我们记住: 从 retryWhen 函数调用后的返回的值 Observable 是通知 Observable

它发出的值并不重要,它只在发出值时才重要,因为这将触发重试尝试。

立即重试控制台输出

如果我们执行上述程序,控制台会输出以下结果:

0dae5e22f8785eca725b177a8c2af567.png

如我们所见:HTTP 请求最初失败,但随后尝试重试,第二次请求成功通过。

我们通过检查网络日志来了解两次尝试之间的延迟:

8101742a9c60d958580b9a2c3ed46e80.png

第二次尝试是在错误发生后立即发出的,正如预期的那样。

延迟重试策略

现在让我们实现另一种错误重试策略,在这种策略中,我们在错误发生后等待 2 秒,然后重试。

此策略对于尝试从某些错误中恢复非常有用,例如:由于服务器高流量而导致失败的网络请求。

在那些错误是间歇性的情况下,我们可以简单地在短时间延迟后重试同一个请求,并且请求可能会经过第二次而没有任何问题。

创建 Observable 的函数 timer

要实现延迟重试策略,我们需要在每个错误发生后两秒发出一个通知 Observable,让我们使用 timer 创建函数创建一个通知 Observable, 这个 timer 函数需要几个参数:

  • 初始延迟值,在此之前不会发出任何值
  • 一个周期性的间隔,希望周期性地发出新值

下图是 timer 的弹珠图:

df361a4ca3a85f79c52b3576b4a832a9.png

第一个值0将在3秒后发出,然后我们每秒都有一个新值。

注意:第二个参数是可选的,如果不传意味着我们的 Observable 将在3秒后只发出一个值(0),然后完成。

这个 Observable 是延迟重试尝试的好的开始,所以让我们看看如何将它与 retryWhendelayWhen 操作符结合起来。

delayWhen 操作符

关于 retryWhen 操作符,我们需要记住:定义通知 Observable 的函数只被调用一次。

所以我们只有一次机会来定义我们的通知 Observable,即当重试尝试应该完成时发出信号。

我们将通过定义通知流来获取错误,并将其应用于 delayWhen 操作符。

假设在下面的弹珠图中,源 Observable a-b-c 是发出错误的流,它会随着时间的推移发出失败的HTTP错误:

39cfe58cb74b3ef482158bf85920d2e2.png

delayWhen 操作符分解

通过上述弹珠图,我们可以了解到 delayWhen 是如何工作的:

  • 每一个输入错误的流都会被延迟,最后出现在输出的 Observable 中
  • 每一个值得延迟都不同,并且以完全灵活的方式创建
  • 为了确定延迟,我们将调用传递给 delayWhen 的函数(称为持续时间选择器函数),并把输入错误的 Observable 的值传入时间选择器函数
  • 持续时间选择器函数将返回一个 Observable,这个 Observable 决定每个输入值的延迟何时结束
  • 每个值 a-b-c 有它自己的持续时间选择器 Observable,它最终将发出一个值(可以是任何值),然后完成
  • 当这些持续时间选择器中的每一个都发出值时,相应的输入值 a-b-c 将出现在 delayWhen 的输出中
  • 注意,值 b 显示在输出值 c 之后,这是正常的
  • c 在 b 之前出现是因为 b 持续时间选择器 Observable(从顶部开始的第三条水平线)是在 c 的持续时间选择器 Observable 之后发出它的值

延迟重试策略的实现

现在,让我们把所有这些放在一起,看看如何在每次错误发生2秒后连续重试失败的 HTTP 请求:

const http$ = this.http.get<Course[]>('/api/courses');

http$.pipe(
        tap(() => console.log("HTTP request executed")),
        map(res => Object.values(res["payload"]) ),
        shareReplay(),
        retryWhen(errors => {
            return errors
                    .pipe(
                        delayWhen(() => timer(2000)),
                        tap(() => console.log('retrying...'))
                    );
        } )
    )
    .subscribe(
        res => console.log('HTTP response', res),
        err => console.log('HTTP Error', err),
        () => console.log('HTTP request completed.')
    );
  • 记住传递给 retryWhen 的函数只会被调用一次
  • 我们在该函数中返回一个 Observable,它将在需要重试时发出值
  • 每当有错误发生,delayWhen 操作符将会通过调用 timer 函数创建一个持续时间选择器的 Observable
  • 这个持续时间选择器 Observable将会再 2s 后发出值(0),发出后并完成
  • 一旦2s后发出来值,delayWhen 的 Observable 就知道给定输入错误的延迟已经过去
  • 只有当延迟过去(错误发生后2秒),错误才会显示在通知 Observable 的输出中
  • 一旦在通知 Observable 中发出一个值,retryWhen 操作符将执行一次重试尝试

重试策略控制台输出

现在让我们看看这个在控制台中是什么样子,下面是一个 HTTP 请求的示例,该请求重试了5次,前4次是错误的:

9c17c2e40bca0fd6641ff79e56c0763b.png

下面是网络日志:

5378685233066e570bb95a0953a0744c.png

重试只发生在错误发生2秒后,如预期的那样!

至此,我们已经完成了一些最常用的RxJS错误处理策略的指导教程,现在让我们总结一下并提供一些运行的示例代码。

Github 仓储 (代码示例)

为了尝试这些多种错误处理策略,有一个可以尝试处理失败的HTTP请求的示例是很重要的。

rxjs-course 包含了一个小带有服务端的运行程序,可以用来模拟 HTTP 错误。

以下是应用程序的外观:

86130cc09c155502fe4512c4f1372c62.png

结束

理解 RxJS 的错误处理就需要首先理解可观察契约的基本原理,需要记住的是:任何给定的流只能出错一次,这是流完成所独有的;这两种情况中只有一种可能发生。

为了从错误中恢复,唯一的方法是以某种方式生成一个替换流作为出错输出流的替代,就像在 catchError 或 retryWhen 运算符的情况下发生的那样。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值