loading...

[Angular][RxJS] I want to return immediatly when some operations are failed

masanori_msl profile image Masui Masanori ・4 min read

Intro

When I write C#, I often write like below.

private async Task ExecuteExampleAsync()
{
    var result1 = await DoSomethingAsync1();
    if (result1 == null)
    {
        return;
    }
    var result2 = await DoSomethingAsync2();
    if (result2 == null)
    {
        return;
    }
    return await DoSomethingAsync3();
}

I want to stop operations and return earlier when I get some invalid result from the pervious operation.

How I can do with RxJS?

I try it.

Environments

  • Angular ver.10.1.0-next.1
  • RxJS ver.6.6

empty, throwError, throw new Error

I think I can use three ways to stop operations.

Base sample class

In this time, I call some methods to try.

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

Once "empty" is called, "complete" handler will be called immediately.

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

result

-- Throw empty from first call --
complete
-- Throw empty from second call --
2nd execution: 4
complete
-- End --

If the method uses "empty", it must handle the result of operations.

Because the caller("ngOnInit()" in this sample) only can know the operation is completed.

empty

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

result

-- 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 --
  • When I don't add "catchError", "error" handler of "subscribe" will be fired.
    Alt Text

  • When I add "catchError", it will be fired and if it return "of(error)", "next" and "complete" handler of "subscribe" will be fired.
    Alt Text

So when I want to handle the invalid value on "catchError" or "error" handler, I can use "throwError".

throw new Error

How about throwing error, or when some errors are occurred in methods?

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

result

-- Throw new Error from first call --
ERROR Error: Error from new Error()

Why I got only the first result?

It's because if errors are occurred in first execution, "catchError" and "error" handler won't be able to handle them.
Alt Text

So if I change the argument from 4 to 3, the result will be like below.

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

Alt Text

So I think the methods(functions) what returns Observable value shouldn't use "throw new Error".

They should notify caller some errors by "throwError".

Should I use "throwError" to notify?

I think if I choose from "empty", "throwError", "throw new Error" I prefer to "throwError".
I still don't know if I should use "throwError" to notify some operations are failed :<

So I will read more samples or documents.

Posted on by:

masanori_msl profile

Masui Masanori

@masanori_msl

Programmer, husband, father I love C#, TypeScript, etc.

Discussion

markdown guide