Tutorial

Simple Error Handling in RxJS

Published on September 21, 2017
    Default avatar

    By Alligator.io

    Simple Error Handling in RxJS

    While we believe that this content benefits our community, we have not yet thoroughly reviewed it. If you have any suggestions for improvements, please let us know by clicking the “report an issue“ button at the bottom of the tutorial.

    Creating complex observable pipelines is all good, but how do you effectively handle errors within them? Let’s go over some of the basics here with the catch, finally, retry and retryWhen operators.

    Observer’s Error Callback

    At its most basic, observers take an error callback to receive any unhandled errors in an observable stream. For example, here our observable fails and an error message is printed to the console:

    const obs$ = Rx.Observable
      .interval(500)
      .map(value => {
        if (value > 3) {
          throw new Error('too high!');
        } else {
          return value;
        }
      });
    
    obs$.subscribe(value => {
      console.log(value);
    },
    err => {
      console.error('Oops:', err.message);
    },
    () => {
      console.log(`We're done here!`);
    });
    

    And here’s what gets printed in the console:

    0
    1
    2
    3
    Oops: too high!
    
    

    The Catch Operator

    Having unhandled errors propagated to the observer should be a last resort, because we can use the catch operator to deal with errors as they happen in the stream. Catch should return another observable or throw again to be handled by the next catch operator or the observer’s error handler if there’s no additional catch operator.

    Here, for example, we return an observable of the value 3:

    const obs$ = Rx.Observable
      .interval(500)
      .map(value => {
        if (value > 3) {
          throw new Error('too high!');
        } else {
          return value;
        }
      })
      .catch(error => {
        return Rx.Observable.of(3);
      });
    
    obs$.subscribe(value => {
        console.log(value);
      },
      err => {
        console.error('Oops:', err.message);
      },
      () => {
        console.log(`We're done here!`);
      });
    

    Here’s what we get at the console. Notice how our main observable stream completes after the observable returned using catch completes:

    0
    1
    2
    3
    3
    We're done here!
    
    

    A stream can have as many catch operators as needed, and it’s often a good idea to have a catch close to a step in the stream that might fail.


    If you want the stream to just complete without returning any value, you can return an empty observable:

    .catch(error => {
      return Rx.Observable.empty();
    })
    

    Alternatively, if you want the observable to keep hanging and prevent completion, you can return a never observable:

    .catch(error => {
      return Rx.Observable.never();
    })
    

    Returning the source observable

    Catch can also take a 2nd argument, which is the source observable. If you return this source, the observable will effectively restart all over again and retry:

    const obs$ = Rx.Observable
      .interval(500)
      .map(value => {
        if (value > 3) {
          throw new Error('too high!');
        } else {
          return value;
        }
      })
      .catch((error, source$) => {
        return source$;
      })
    

    This is what we get:

    0
    1
    2
    3
    0
    1
    2
    3
    0
    1
    2
    3
    ...
    
    

    You’ll want to be careful and only return the source observable when errors are intermittent. Otherwise if the stream continues failing you’ll create an infinite loop. For more flexible retrying mechanisms, see below about retry and retryWhen

    Finally

    You can use the finally operator to run an operation no matter if an observable completes successfully or errors-out. This can be useful to clean-up in the case of an unhandled error. The callback function provided to finally will always run. Here’s a simple example:

    const obs$ = Rx.Observable
      .interval(500)
      .map(value => {
        if (value > 3) {
          throw new Error('too high!');
        } else {
          return value;
        }
      })
      .finally(() => {
        console.log('Goodbye!');
      });
    
    obs$.subscribe(value => {
      console.log(value);
    },
    err => {
      console.error('Oops:', err.message);
    },
    () => {
      console.log(`We're done here!`);
    });
    

    This ouputs the following:

    0
    1
    2
    3
    Oops: too high!
    Goodbye!
    
    

    Retrying

    The retry operator

    You can use the retry operator to retry an observable stream from the beginning. Without an argument, it will retry indefinitely, and with an argument passed-in, it’ll retry for the specified amount of times.

    In the following example, we retry 2 times, so our observable sequence runs for a total of 3 times before finally propagating to the observer’s error handler:

    const obs$ = Rx.Observable
      .interval(500)
      .map(value => {
        if (value > 3) {
          throw new Error('too high!');
        } else {
          return value;
        }
      })
      .retry(2)
    
    
    obs$.subscribe(value => {
      console.log(value);
    },
    err => {
      console.error('Oops:', err.message);
    },
    () => {
      console.log(`We're done here!`);
    });
    

    Here’s the outputted result:

    0
    1
    2
    3
    0
    1
    2
    3
    0
    1
    2
    3
    Oops: too high!
    
    

    You can also add a catch right after a retry to catch an error after a retry was unsuccessful:

    .retry(1)
    .catch(error => {
      return Rx.Observable.of(777);
    });
    
    0
    1
    2
    3
    0
    1
    2
    3
    777
    We're done here!
    
    

    The retryWhen operator

    Using the retry operator is all well and good, but often we want to retry fetching data from our backend, and if it just failed, we probably want to give it a little time before retrying again and taxing the server unnecessarily. The retryWhen operator allows us to do just that. retryWhen takes an observable of errors, and you can return that sequence with an additional delay to space-out the retries.

    Here we wait for 500ms between retries:

    const obs$ = Rx.Observable
      .interval(500)
      .map(value => {
        if (value > 3) {
          throw new Error('too high!');
        } else {
          return value;
        }
      })
      .retryWhen(error$ => {
        return error$.delay(500);
      });
    

    The above code will retry forever if the error keep happening. To retry for a set amount of times, you can use the scan operator to keep track of how many retries have been made and throw the error further down the chain if the amount of retries exceeds a certain number.

    Here on the 4th retry, we’ll give up and let the error propagate to the observer:

    const obs$ = Rx.Observable
      .interval(500)
      .map(value => {
        if (value > 3) {
          throw new Error('too high!');
        } else {
          return value;
        }
      })
      .retryWhen(error$ => {
        return error$.scan((count, currentErr) => {
          if (count > 3) {
            throw currentErr;
          } else {
            return count += 1;
          }
        }, 0);
      });
    

    Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

    Learn more about us


    About the authors
    Default avatar
    Alligator.io

    author

    Still looking for an answer?

    Ask a questionSearch for more help

    Was this helpful?
     
    Leave a comment
    

    This textbox defaults to using Markdown to format your answer.

    You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!

    Try DigitalOcean for free

    Click below to sign up and get $200 of credit to try our products over 60 days!

    Sign up

    Join the Tech Talk
    Success! Thank you! Please check your email for further details.

    Please complete your information!

    Get our biweekly newsletter

    Sign up for Infrastructure as a Newsletter.

    Hollie's Hub for Good

    Working on improving health and education, reducing inequality, and spurring economic growth? We'd like to help.

    Become a contributor

    Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

    Welcome to the developer cloud

    DigitalOcean makes it simple to launch in the cloud and scale up as you grow — whether you're running one virtual machine or ten thousand.

    Learn more
    DigitalOcean Cloud Control Panel