vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

【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」ハンドラーが呼ばれたことしか返ってこないからですね。 f:id:mslGt:20200718144847p:plain

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」ハンドラーが呼ばれます。 f:id:mslGt:20200718144926p:plain

  • 「catchError」を使っている場合、「catchError」で「of(error)」を返すと通常と同じく「next」と「complete」が呼ばれます。 f:id:mslGt:20200718144947p:plain

ということで、処理に失敗したことを 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」では受け取ることができず、処理が止まってしまうからですね。 f:id:mslGt:20200718145020p:plain

引数を2回目の実行時に Error を投げることになる 3 に変更すると、下記のような結果になります。

-- Throw new Error from first call --
2nd execution: 4
error: Error: Error from new Error()
...

f:id:mslGt:20200718145049p:plain

ということで、 Observable を返すメソッド(関数)では「throw new Error」は使わない方が良さそうです。

また、メソッド内で Error が発生する場合も呼び出し元には「throwError」として返す必要があるかと。

処理失敗の通知に「throwError」を使うべきか

今回試した「empty」、「throwError」、「throw new Error」の中では「throwError」を使うのがよさそうです。

が、まだ処理失敗の通知に「throwError」を使うべきか、というのはよくわかっておりません。

もう少しサンプルなど漁った方がよさそうですね。