RxJs——错误处理(一)

时间:2022-06-01 12:40:32

错误处理是RxJs中重要组成部分。我们在响应式编程中几乎都要使用到。然而RxJs中的错误处理不像在其他库中的错误处理那么容易理解。小窍门就是关注Observable的约定,这样就容易理解RxJs中错误处理。

 

今天我们介绍一些常见的错误处理策略,涵盖一些常用场景,当然还是从Observable的基础知识——Observable的约定。

Observable约定

首先我们需要理解的是给定的流只能出错一次。这是Observable的约定定义的,且它能发送一个或者多个值。定义成这样的约定也是符合在实践中观察到的流的工作方式。

给定的流可能完成,也可能失败;流完成意味着:

1、流没有错误的结束其生命周期。

2、完成后,流不再进一步发出任何值。

流完成的替代形式是流失败,这意味着:

1、流以错误结束其生命周期。

2、错误被抛出以后,流不再进一步发出任何值。

注意,流的完成和出错是互斥的:

1、如果流完成,之后不会出错。

2、如果流出错,之后不会完成。

流没有义务完成或者出错,这两种可能性是可选的。这两种情况只能发生一种,不能同时都发生。因此,根据Observable的约定,当一个特定的流出错时,我们不能再使用它;因此,我们需要思考的是怎么样从错误的流中恢复?

CatchError错误处理图示

RxJs——错误处理(一)

 

RxJs的订阅和错误回调函数

要查看RxJs错误处理行为的实际效果,我们创建一个流,并且订阅它。我们知道,订阅调用需要三个可选参数:

成功处理器函数,每次流发出一个值都会调用它。

错误处理器函数,只在错误发生时调用一次。处理器函数的参数接收错误本身。

完成处理器函数,只在流完成时调用一次。

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.')
        );
    }
}

如果流没有发生错误,那么将在控制台上看到:

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

如上所示,这个流值发出一个值,然后完成,意味着没有发生任何错误。但是如果流抛出了错误,那么将在控制台上看到

RxJs——错误处理(一)

如图所示,流没有发出任何值,它立即出错了。出错后,没有完成。

 

订阅处理器函数的限制

有时我们只需要使用 subscribe 调用处理错误,但是这种错误处理方法是有限制的。例如,使用这种方法,我们无法从错误中恢复或发出替代备用值来替换我们期望从后端得到的值。让我们学习一些运算符,它们将使我们能够实现一些更高级的错误处理策略。

 

catchError操作符原理

和任何 RxJs 运算符一样,catchError 只是一个简单的函数,它接收一个输入 Observable,并输出一个 Output Observable。

每次调用 catchError 时,我们都需要向它传递一个函数,我们将调用该函数来调用错误处理函数。

catchError 操作符将一个可能出错的 Observable 作为输入,并开始在其输出 Observable 中发出输入 Observable 的值。如果没有发生错误,catchError 产生的输出 Observable 的工作方式与输入 Observable 完全相同。

抛出错误会发生什么呢?

如果发生错误,则 catchError 逻辑将启动。catchError 运算符将获取错误并将其传递给错误处理函数。该函数预计将返回一个 Observable,它将成为刚刚出错的流的替换Observable。让我们记住 catchError 的输入流已经出错了,所以根据 Observable 合约我们不能再使用它了。然后将订阅此替换Observable,并且将使用其值代替错误输出的输入Observable。(产生一个替换Observable,并且被订阅,返回此替换Observable

 

捕获且替换策略

代码例子

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

http$错误处理函数不会立即调用,一般情况下通常不会调用
        catchError(err => of([]))
    )
    .subscribe(
        res => console.log('HTTP response', res),
        err => console.log('HTTP Error', err),
        () => console.log('HTTP request completed.')
    );

我们分解一下这种策略的步骤

1、我们正在向 catchError 运算符传递一个函数,它是错误处理函数

2、错误处理函数不会立即调用,一般情况下通常不会调用

3、只有当 catchError 的输入 Observable 发生错误时,才会调用错误处理函数

4、如果输入流中发生错误,则此函数将返回使用 of([]) 函数构建的 Observable(替换Observable)

5、of() 函数构建一个 Observable,它只发出一个值 ([]),然后完成

6、错误处理函数返回由 catchError 运算符订阅的恢复 Observable (of([]))

7、然后,在 catchError 返回的输出 Observable 中将恢复 Observable 的值作为替换值发出

最终结果是,http$ Observable 将不再出错!这是我们在控制台中得到的结果:

HTTP response []
HTTP request completed.

正如我们所见,subscribe() 中的错误处理回调不再被调用。相反,会发生以下情况:

  • 发出空数组值 []
  • http$ Observable 然后完成

正如我们所见,替换的 Observable 被用来为 http$ 的订阅者提供一个默认的回退值 ([]),尽管原来的 Observable 确实出错了。

请注意,在返回替换的 Observable 之前,我们还可以添加一些本地错误处理!

这涵盖了 Catch and Replace 策略,现在让我们看看我们如何也可以使用 catchError 来重新抛出错误,而不是提供备用值。

 

捕获和重抛出策略

我们首先注意到通过 catchError 提供的替换 Observable 本身也会出错,就像任何其他 Observable 一样。如果发生这种情况,错误将传播到 catchError 的输出 Observable 的订阅者。这种错误传播行为为我们提供了一种机制,可以在本地处理错误后重新抛出 catchError 捕获的错误。我们可以通过以下方式做到这一点:

代码例子

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

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

捕获并重新抛出分析

1、就像之前一样,我们正在捕捉错误,并返回一个替换的 Observable

2、但是这一次,我们在 catchError 函数中本地处理错误,而不是提供像 [] 这样的替换输出值

3、在这种情况下,我们只是将错误记录到控制台,但我们可以添加任何我们想要的本地错误处理逻辑,例如向用户显示错误消息

4、然后我们返回一个替换的 Observable,这一次是使用 throwError 创建的

5、throwError 创建一个从不发出任何值的 Observable。相反,它立即发出使用 catchError 捕获的相同错误

6、这意味着 catchError 的输出 Observable 也会出错,与 catchError 的输入抛出的错误完全相同

7、这意味着我们已经成功地将 catchError 的输入 Observable 最初抛出的错误重新抛出到它的输出 Observable

8、如果需要,该错误现在可以由 Observable 链的其余部分进一步处理

 

如果我们现在运行上面的代码,这是我们在控制台中得到的结果:

RxJs——错误处理(一)

正如我们所看到的,正如预期的那样,在 catchError 块和订阅错误处理函数中都记录了相同的错误。

 

在Observable链中多次使用catchError

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

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

代码例子

onst 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.')
    );

如果我们运行上面的代码,这是我们在控制台中得到的输出:

RxJs——错误处理(一)

如我们所见,最初确实重新抛出了错误,但它从未到达订阅错误处理函数。相反,正如预期的那样,发出了回调 [] 值。

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 块是如何执行的:

RxJs——错误处理(一)

请注意,最后一个 finalize 块在订阅值处理程序和完成处理程序函数之后执行。

 

明天继续分析重试策略,今天主要介绍四种错误处理情况。