Error Handling
catchError
finalize
retry
retryWhen
Reading about error handling in programming, seems like a good
JavaScript Error Handling
Traditional Try/Catch => only synchronous problem
Error Callback => Error no inversion of control
Promise Catch function => catch chaining :class but not fine-tuned strategy
Try/catch:
Can’t be composed or chained like other functional artifacts
Violate the principle of pure functions that advocates a single, predictable value because throwing exceptions
constitutes another exit path from your function calls
Violate the principle of non-locality because the code used to recover from the error is distanced from the originating
function call
Are hard to use when multiple error conditions create nested levels of exception handling blocks
Promise:
If an instance of Try"Record" represents a successful computation, it’s an instance of Success"Record" internally
that’s used to continue the chain
If, on the other hand, it represents a computation in which an error has occurred, it’s an instance of Failure"Error"
, wrapping an Error object or an exception
=> CF Promise slide in Paradigm chapter
Observable Error
rxjs.of('test', 'test error')
.pipe(
rxjs.operators.map(str => {
if(str.includes(' ')) {
throw new Error(`Error trailing space: ${str}`)
}
return str;
})
)
.subscribe(
(res) => console.log(res),
(err) => console.log(`[Error]: ${err}`),
() => console.log('Observable completed!')
)
In general, errors don’t escape the observable pipeline. They are contained and guarded to prevent side effects from happening
Errors that occur at the beginning of the stream or in the middle are propagated down to any observers, finally
resulting in a call to error()
the first exception that fires will result in the entire stream being cancelled
Example trace: $ test $ [Error]: Error: Error trailing space: test error
CatchError Operator
rxjs.of(2,3,4,6)
.pipe(
rxjs.operators.map(num => {
if(num % 2 !== 0) {
throw new Error(`Unexpected odd number: ${num}`)
}
return num;
}),
rxjs.operators.catchError(err => rxjs.of(0)),
rxjs.operators.map(num => ++num)
)
.subscribe(
(res) => console.log(res),
(err) => console.log(`[Error]: ${err}`),
() => console.log('Observable completed!')
)
Catches errors on the observable to be handled by returning a new observable or throwing an error
First example trace: //$ 3 //$ 1 //$ Observable completed!
Even after the use of a catch that returns a default value the stream continues to be cancelled when the exception
occurs
However, some errors, might be intermittent and shouldn’t halt the stream
For instance, a server is unavailable for a short period of time because of a planned outage. In cases like this,
you may want to retry your failed operations
Retry Operator
rxjs.of(2,3,4,6)
.pipe(
rxjs.operators.map(num => {
if(num % 2 !== 0) {
throw new Error(`Unexpected odd number: ${num}`)
}
return num;
}),
rxjs.operators.map(num => ++num)
rxjs.operators.catchError((err) => rxjs.of(0))
)
.subscribe(
(res) => console.log(res),
(err) => console.log(`Caught: ${err}`),
() => console.log('Observable completed!')
)
Returns an Observable that mirrors the source Observable with the exception of an error.
If the source Observable calls error, this method will resubscribe to the source Observable for a maximum of count resubscriptions (given as a number parameter) rather than propagating the error call
Multiple strategies just by compose our catch retry operator:
catch => next() -> complete() stop
retry => error() stop
retry then catch => next() -> complete() stop
Promise Immutability
mySource$
.pipe(
rxjs.operators.switchMap((url) => callPromise(url)),
rxjs.operators.retry(3)
)
Don't forget we don’t get second chances with Promises due to the immutability principle
Solution: again creating a higher order observable
The code recreate a new Promise
??? Because Promises are not retriable artifacts, dereferencing the value of a Promise will always return its fulfilled
value or error, as the case may be
Backoff strategy
No particular strategy => retry Operator
Linear backoff => retryWhen Operator
Exponential backoff => retryWhen Operator
Backoff is an effective way to retry more times without overloading the server.
Backoff strategy:
constant
linear
exponential
random (aka jitter strategy)
The goal is to use progressively longer waits between retries for consecutive periods of time
exponential & linear more commonly used
retryWhen Operator + Timer Observable => more common to accomplish a strategy
RetryWhen Operator
// linear backoff strategy
.pipe(rxjs.operators.retryWhen(errors$ =>
rxjs.range(1, maxRetries)
.pipe(
rxjs.operators.zip(errors$, (i, err) => ({'i': i, 'err': err})),
rxjs.operators.mergeMap(({i, err}) =>
Rx.Observable.if(() => i <= maxRetries - 1,
rxjs.timer(i * 1000)
.pipe(rxjs.operators.tap(() => console.log(`Retrying after ${i} second(s)...`))),
rxjs.throw(err))
)
)
)
Returns an Observable that mirrors the source Observable with the exception of an error.
If the source Observable calls error, this method will emit the Throwable that caused the error to the Observable returned from notifier. If that Observable calls complete or error then this method will call complete or error on the child subscription. Otherwise this method will resubscribe to the source Observable.
Replay vs Subscribe
Replay the same stream in other words keep the same side effect value Whereas, subscribe regenerate a new sequence => Promise
example
Finalize Operator
const source$ = rxjs.of(1,2,3,4,5)
.finalize(() => console.log('FINALIZE'))
.subscribe(
(res) => console.log(res),
(err) => console.log(`[Error]: ${err}`),
() => console.log('Observable completed!')
)
Returns an Observable that mirrors the source Observable, but will call a specified function when the source terminates on complete or error
Using finally to clean up and cancel any outstanding streams
Be careful, finally is called at the same time complete() || error() Observer method