Skip to content

Error Handling

Vitaly Tomilov edited this page Aug 4, 2024 · 65 revisions

When used for synchronous iterables, you can resort to the regular try/catch at the point where iteration is triggered in your code. This however presents some problems:

  • Reusable iterables can trigger different errors, depending on usage + input data, which means manual error handling will be required at every point of triggering an iteration.
  • Verbose error-handling coding around every use of iterables is a fairly poor and time-consuming practice.
  • It doesn't really work for asynchronous iterables, which this library fully supports

That's why here we are covering error handling strictly during/inside iteration, as values are retrieved one by one. This library supports explicit + implicit ways of handling iteration errors, as documented below.

Explicit error handling

You can handle all upstream iteration errors with a catchError operator at the end of the pipeline:

pipe(iterable, ...operators, catchError((error, ctx) => {
    // Handling the error (logging it, etc)

    // After that, you can do any of the following:
    //
    //  - nothing (we let it skip the value);
    //  - provide a new/alternative value (via ctx.emit(value));
    //  - re-throw the original error;
    //  - throw a new error.
}))

Parameters passed into the error handler:

  • error - the original error that was thrown;
  • ctx - iteration error context - see IErrorContext type.

When the handler wants to provide an alternative iteration value, it has to call ctx.emit(value).

Implicit error handling

You can use catch on any piped iterable:

pipe(iterable, ...operators)
    .catch((error, ctx) => {
    });

This will simply append catchError to the end of the pipeline, so the result is the same as with the explicit error handling. However, this adds the flexibility of handling errors separately from the pipeline implementation.

Chaining error handlers

You can chain as many error handlers as you want, each handling what's upstream, after the last error handler.

For explicit error handling, this means you can have any number of catchError operators inside the pipeline:

pipe(iterable, operator1, catchError, operator2, operator3, catchError);
//=> first catchError will handle errors thrown by operator1
//=> second catchError will handle errors thrown by operator2 and operator3
//=> second catchError will handle all errors, if the first catchError re-throws

For implicit error handling, this means you can chain any number of catch handlers at the end of the pipeline, each will be appending another catchError to the end of the pipeline:

pipe(iterable, ...operators)
    .catch() // first handler
    .catch() // second handler
    .catch() // third handler

// this will produce:
// pipe(iterable, ...operators, catchError, catchError, catchError);

Tips

Error handling here is for the iteration itself. When an iteration error occurs, it usually means method next in one of the iterators threw an error, and as such, it will continue doing so repeatedly. So, if you just handle the error, but without re-throwing it, you are likely to end up with the same error being reported in an endless loop.

There are two ways to avoid it...

First, you can simply re-throw the error:

.catch(err => {
    console.log(err); // log the error somewhere
    throw e; // re-throw the error, to exit the iteration
})

A smarter way is to re-throw after detecting a repeated error. This is why we have repeats property in the error context object:

.catch((err, ctx) => {
    if(ctx.repeats) {
        throw e; // re-throw when repeated
    }
    console.log(err); // log the error somewhere
})
Clone this wiki locally