【RxJS】処理に失敗したときに早期リターンしたい話
はじめに
C# でコードを書く時に、下記のような処理をよく書きます。
private async Task ExecuteExampleAsync() { var result1 = await DoSomethingAsync1(); if (result1 == null) { return; } var result2 = await DoSomethingAsync2(); if (result2 == null) { return; } return await DoSomethingAsync3(); }
例えば一つ目の処理に失敗した場合はそこで処理をストップする、という話なのですが、これを RxJS でやるとどうなるの?というのが前から気になっていたので試してみました。
Environments
- Angular ver.10.1.0-next.1
- RxJS ver.6.6
empty, throwError, throw new Error
今回調べてみたなかで、実現できそうな方法は3つありました。
サンプルコード(ベースだけ)
今回は下記の ngOnInit からメソッドを呼ぶ、という形で試すことにしました。
workflow-page.component.ts
import { Component, OnInit } from '@angular/core'; import { Observable, of, empty, throwError } from 'rxjs'; import { flatMap, catchError } from 'rxjs/operators'; @Component({ selector: 'app-workflow-page', templateUrl: './workflow-page.component.html', styleUrls: ['./workflow-page.component.css'] }) export class WorkflowPageComponent implements OnInit { constructor() { } ngOnInit(): void { // call methods } }
empty
「empty」が呼ばれると、subscribe の「complete」ハンドラーが呼ばれて処理が完了します。
... ngOnInit(): void { console.log('-- Throw empty from first call --'); this.executeEmpty(4); console.log('-- Throw empty from second call --'); this.executeEmpty(3); console.log('-- End --'); } private executeEmpty(startValue: number) { this.getEmpty(startValue) .pipe( flatMap(result => { console.log(`2nd execution: ${result}`); return this.getEmpty(result); }), catchError(error => { console.error(`catch: ${error}`); return of(error); }) ) .subscribe(result => console.log(`next: ${result}`), error => console.error(`error: ${error}`), () => console.log('complete')); } private getEmpty(lastValue: number): Observable<number> { if (lastValue > 3) { return empty(); } return of(lastValue + 1); }
結果
-- Throw empty from first call -- complete -- Throw empty from second call -- 2nd execution: 4 complete -- End --
以上から、「empty」を使う場合、処理の結果(今回は null が返ってきた)は呼び出し先である「getEmpty」がハンドリングする必要があります。
呼び出し元には「complete」ハンドラーが呼ばれたことしか返ってこないからですね。
throwError
... ngOnInit(): void { console.log('-- Throw throwError from first call --'); this.executeThrowError(4); console.log('-- Throw throwError from second call --'); this.executeThrowError(3); console.log('-- Throw throwError with catchError from first call --'); this.executeThrowErrorWithCatchError(4); console.log('-- Throw throwError with catchError from second call --'); this.executeThrowErrorWithCatchError(3); console.log('-- End --'); } private executeThrowError(startValue: number) { this.getThrowError(startValue) .pipe( flatMap(result => { console.log(`2nd execution: ${result}`); return this.getThrowError(result); }) ) .subscribe(result => console.log(`next: ${result}`), error => console.error(`error: ${error}`), () => console.log('complete')); } private executeThrowErrorWithCatchError(startValue: number) { this.getThrowError(startValue) .pipe( flatMap(result => { console.log(`2nd execution: ${result}`); return this.getThrowError(result); }), catchError(error => { console.error(`catch: ${error}`); return of(error); }) ) .subscribe(result => console.log(`next: ${result}`), error => console.error(`error: ${error}`), () => console.log('complete')); } public getThrowError(lastValue: number): Observable<number> { if (lastValue > 3) { return throwError('Error from throwError'); } return of(lastValue + 1); }
結果
-- Throw throwError from first call -- error: Error from throwError ... -- Throw throwError from second call -- 2nd execution: 4 error: Error from throwError ... -- Throw throwError with catchError from first call -- catch: Error from throwError ... next: Error from throwError complete -- Throw throwError with catchError from second call -- 2nd execution: 4 catch: Error from throwError ... next: Error from throwError complete -- End --
「catchError」を使っていない場合はsubscribeの「error」ハンドラーが呼ばれます。
「catchError」を使っている場合、「catchError」で「of(error)」を返すと通常と同じく「next」と「complete」が呼ばれます。
ということで、処理に失敗したことを subscribe でエラーとしてハンドリングしたい場合は「catchError」無しで「throwError」を使うか、「catchError」からさらに「throwError」を実行する、といった書き方になりそうです。
throw new Error
「throw new Error」で Error を投げる、または呼び出したメソッド内で Error が発生した場合はどうでしょうか。
... ngOnInit(): void { console.log('-- Throw new Error from first call --'); this.executeThrowNewError(4); console.log('-- Throw new Error from second call --'); this.executeThrowNewError(3); } private executeThrowNewError(startValue: number) { this.getThrowNewError(startValue) .pipe( flatMap(result => { console.log(`2nd execution: ${result}`); return this.getThrowNewError(result); }) ) .subscribe(result => console.log(`next: ${result}`), error => console.error(`error: ${error}`), () => console.log('complete')); } // throw new Error public getThrowNewError(lastValue: number): Observable<number> { if (lastValue > 3) { throw new Error('Error from new Error()'); } return of(lastValue + 1); }
結果
-- Throw new Error from first call -- ERROR Error: Error from new Error()
最初の結果しか出力されない。。。(´・ω・`)
というのも、 pipe でつながれていない、最初の処理で Error が発生してしまうと「catchError」や subscribe の「error」では受け取ることができず、処理が止まってしまうからですね。
引数を2回目の実行時に Error を投げることになる 3 に変更すると、下記のような結果になります。
-- Throw new Error from first call -- 2nd execution: 4 error: Error: Error from new Error() ...
ということで、 Observable を返すメソッド(関数)では「throw new Error」は使わない方が良さそうです。
また、メソッド内で Error が発生する場合も呼び出し元には「throwError」として返す必要があるかと。
処理失敗の通知に「throwError」を使うべきか
今回試した「empty」、「throwError」、「throw new Error」の中では「throwError」を使うのがよさそうです。
が、まだ処理失敗の通知に「throwError」を使うべきか、というのはよくわかっておりません。
もう少しサンプルなど漁った方がよさそうですね。