javascriptrxjsswitchmap

RxJS: Handle cancelled events when using `switchMap` operator


Consider the following snippet

const { NEVER, timer } = rxjs;
const { catchError, switchMap, timeout } = rxjs.operators;

timer(0, 3000).pipe(
  switchMap(() => 
    timer(randomIntFromInterval(1, 4) * 1000).pipe(  // <-- mock HTTP call
      catchError(() => {
        // do something on error
        console.log('Caught error');
        return NEVER;
      })
    )
  ),
).subscribe({
  next: (value) => console.log('Next triggred')
});

// credit: https://stackoverflow.com/a/7228322/6513921
function randomIntFromInterval(min, max) {
  const value = Math.floor(Math.random() * (max - min + 1) + min);
  console.log(`Simulated HTTP call ${value}s`);
  return value;
}
.as-console-wrapper { max-height: 100% !important; top: 0px }
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/7.4.0/rxjs.umd.min.js"></script>

Here the catchError would only be triggered when the HTTP call emits an error. But if the HTTP call doesn't return anything within the 3 seconds poll timer, the previous request would be cancelled before the next call. I'd like to perform error-handling (essentially triggering the catchError operator) over these cancelled requests.

I'm aware we could pipe in a timeout with < 3s threshold to throw an error. But I'd like to handle it without using timeout operator.

Could anyone come up with a better solution? TIA.


Solution

  • I can suggest slightly different approach: instead of throwing an error you can just track such cases and apply the logic you need

    Here's an operator to do this:

    function switchMapWithOvertakeEvent<T, R>(
      project: (value: T, index: number) => ObservableInput<R>,
      onOvertake: (value: T) => void
    ): OperatorFunction<T, R> {
      let awaitingResponse = false;
      return (src$) =>
        src$.pipe(
          tap((v) => {
            if (awaitingResponse) {
              onOvertake(v);
            }
            awaitingResponse = true;
          }),
          switchMap(project),
          tap(() => (awaitingResponse = false))
        );
    }
    

    It can be used with your example as follows

    timer(0, 3000)
      .pipe(
        switchMapWithOvertakeEvent(
          () =>
            timer(randomIntFromInterval(1, 10) * 1000).pipe(
              // <-- mock HTTP call
              catchError(() => {
                // do something on error
                console.log('Caught error');
                return NEVER;
              })
            ),
          () => console.log('http call cancelled')
        )
      )
      .subscribe({
        next: (value) => console.log('Next triggred'),
        complete: () => console.log('complete'),
      });
    
    // credit: https://stackoverflow.com/a/7228322/6513921
    function randomIntFromInterval(min, max) {
      const value = Math.floor(Math.random() * (max - min + 1) + min);
      console.log(`Simulated HTTP call ${value}s`);
      return value;
    }
    

    You can play with the code here https://stackblitz.com/edit/mjemgq?devtoolsheight=50