I would like to devise a scalable error handling strategy for my Observable-based Firebase requests. To do so, I am trying to create a custom RxJS operator for each type of error (e.g. unauthenticated, internal, you name it). The operators would be stacked after the request as follows:
FirebaseRequestObservable.pipe(
...
handleUnauthenticatedError(),
handleInternalError(),
handleYouNameItError(),
...
)
This works fine if my error handling operators only constitute of a catchError operator, in which case its internal structure can simply be:
source.pipe(
catchError((err) => {
if (err !== errorThisOperatorShouldHandle) {
throw err
}
handleError
}
))
So that the error trickles down to the next custom operator if it shouldn't be handled by that operator.
The problem comes in when my error handling logic for a certain error involves using retryWhen. I do not know how I could then have the operator conditionally employ retryWhen if it is the right error, and re-throw the error to the next operator if it isn't.
They're pretty similar.
Here's how I might implement these two:
Handle a specific error.
function handleYouNameItError<T>(): MonoTypeOperatorFunction<T> {
return catchError(err => {
if(err !== youNameItError){
return throwError(() => err);
}else{
// handleError
return EMPTY;
}
});
}
Retry a specific error.
function retryYouNameItAnotherError<T>(): MonoTypeOperatorFunction<T> {
return retryWhen(err$ => err$.pipe(
tap(err => {
if(err !== youNameItAnotherError){
throw err;
}
}),
// Delay 1s between retries
delay(1000),
// Only retry 5 times
take(5),
// After 5 retries, throw a new error
concatWith(throwError(() =>
new YouNameItAnotherErrorFailedAfterRetries()
))
));
}
Use them as operators :)
FirebaseRequestObservable.pipe(
...
handleYouNameItError(),
retryYouNameItAnotherError()
...
);
Related
When dealing with concatMap, how can I abort the execution of observables further down the line and prevent calling the completion handler?
Here is a simple example.
of(...[1, 2, 3]).pipe(
concatMap(t => of(t)),
map(n => {
console.log(n);
if (n === 2) {
throw new Error('OK, fail here');
}
}),
catchError((e, c) => of(console.log('Caught ' + e)))
)
.subscribe(
{
complete: () => console.log('Complete should not be triggered in an error case, but here it is'),
error: (err: any) => {
console.log('I did never trigger, thats ok!');
}
});
A source observable emits 1,2,3 which is piped into concatMap.
There are two possible scenarios: All three observables are emitted without error in this case complete handler should be triggered. Or the other case depicted here: Somewhere down the line there is an error like when n === 2. ConcatMap stops executing the next observable which is perfect but it still triggers the completion handler, which is undesired.
Actual result
1
2
Caught Error: OK, fail here
Complete should not be triggered in an error case, but here it is
Desired result
1
2
Caught Error: OK, fail here
Any hints? My previous attempt was to throw in the next handler but that turned out to be really bad :/ as it triggered a hostReportError
Ok,
since I was able to answer all the other fun questions regarding RxJS by myself, here we go :)
Remember: catchError replaces the faulted observable by some other observable. If catchError would therefore return an observable via throwError it would halt the execution and even better, it would trigger the error handler that was idle before.
of(...[1, 2, 3]).pipe(
concatMap(t => of(t)),
map(n => {
console.log(n);
if (n === 2) {
throw new Error('OK, fail here');
}
}),
catchError((e, c) => throwError(e)) // This line has changed
)
.subscribe(
{
complete: () => console.log('Complete should not be triggered in an error case, but here it is'),
error: (err: any) => {
console.log('Now I have something to do, yay!');
}
});
I'm making a request to a 3rd party API via NestJS's built in HttpService. I'm trying to simulate a scenario where the initial call to one of this api's endpoints might return an empty array on the first try. I'd like to use RxJS's retryWhen to hit the api again after a delay of 1 second. I'm currently unable to get the unit test to mock the second response however:
it('Retries view account status if needed', (done) => {
jest.spyOn(httpService, 'post')
.mockReturnValueOnce(of(failView)) // mock gets stuck on returning this value
.mockReturnValueOnce(of(successfulView));
const accountId = '0812081208';
const batchNo = '39cba402-bfa9-424c-b265-1c98204df7ea';
const response =client.viewAccountStatus(accountId, batchNo);
response.subscribe(
data => {
expect(data[0].accountNo)
.toBe('0812081208');
expect(data[0].companyName)
.toBe('Some company name');
done();
},
)
});
My implementation is:
viewAccountStatus(accountId: string, batchNo: string): Observable<any> {
const verificationRequest = new VerificationRequest();
verificationRequest.accountNo = accountId;
verificationRequest.batchNo = batchNo;
this.logger.debug(`Calling 3rd party service with batchNo: ${batchNo}`);
const config = {
headers: {
'Content-Type': 'application/json',
},
};
const response = this.httpService.post(url, verificationRequest, config)
.pipe(
map(res => {
console.log(res.data); // always empty
if (res.status >= 400) {
throw new HttpException(res.statusText, res.status);
}
if (!res.data.length) {
this.logger.debug('Response was empty');
throw new HttpException('Account not found', 404);
}
return res.data;
}),
retryWhen(errors => {
this.logger.debug(`Retrying accountId: ${accountId}`);
// It's entirely possible the first call will return an empty array
// So we retry with a backoff
return errors.pipe(
delayWhen(() => timer(1000)),
take(1),
);
}),
);
return response;
}
When logging from inside the initial map, I can see that the array is always empty. It's as if the second mocked value never happens. Perhaps I also have a solid misunderstanding of how observables work and I should somehow be trying to assert against the SECOND value that gets emitted? Regardless, when the observable retries, we should be seeing that second mocked value, right?
I'm also getting
: Timeout - Async callback was not invoked within the 5000ms timeout specified by jest.setTimeout.Timeout - Async callback was not invoked within the 5000ms timeout specified by jest.setTimeout.Error:
On each run... so I'm guessing I'm not calling done() in the right place.
I think the problem is that retryWhen(notifier) will resubscribe to the same source when its notifier emits.
Meaning that if you have
new Observable(s => {
s.next(1);
s.next(2);
s.error(new Error('err!'));
}).pipe(
retryWhen(/* ... */)
)
The callback will be invoked every time the source is re-subscribed. In your example, it will call the logic which is responsible for sending the request, but it won't call the post method again.
The source could be thought of as the Observable's callback: s => { ... }.
What I think you'll have to do is to conditionally choose the source, based on whether the error took place or not.
Maybe you could use mockImplementation:
let hasErr = false;
jest.spyOn(httpService, 'post')
.mockImplementation(
() => hasErr ? of(successView) : (hasErr = true, of(failView))
)
Edit
I think the above does not do anything different, where's what I think mockImplementation should look like:
let err = false;
mockImplementation(
() => new Observable(s => {
if (err) {
s.next(success)
}
else {
err = true;
s.next(fail)
}
})
)
I've got an Api call that is converted to a promise. My handleError function inside the observable re-throws via throwError. This re-thrown error does not trigger any catch in the outer Promise chain.
callApi() {
return this.http.get(`${this.baseUrl}/someapi`)
.pipe(
map((data: any) => this.extractData(data)),
catchError(error => this.handleError(error))
).toPromise();
handleError(error) {
console.error(error);
return throwError(error || 'Server error');
}
Calling code...
this.someService.callApi()
.then((response) => {
// THIS GETS CALLED AFTER throwError
// do something cool with response
this.someVar = response;
})
.catch((error) => {
// WE NEVER GET TO HERE, even when I force my api to throw an error
console.log(`Custom error message here. error = ${error.message}`);
this.displayErrorGettingToken();
});
Why doesn't the throwError trigger the Promise catch?
You should not use toPromise() when possible.
Use subscribe instead of then.
Also when you catch the error in a pipe it won't be thrown in then because you already caught it, also when you throw the error in a catch error, it won't be emitted into the regular pipe flow of your response.
callApi() {
return this.http.get(`${this.baseUrl}/someapi`);
}
This is totally ok. Http.get() returns a singleton observable stream, which emits only ONE value and then completes. Subscribe to the Observable.
this.someService.callApi()
.subscribe((response) => {
// THIS GETS CALLED always wenn everything is ok
this.someVar = response;
},
(error:HttpErrorResponse) =>{
console.log(`Custom error message here. error ${error.message}`);
this.displayErrorGettingToken();
});
Observable is like an extended version of promise. Use it.
A "authenticationService" provides the following authenticate method. I'm unable to enter the piped catchError. What am I missing?
authenticate(credentials: { username: string; password: string }){
return new Observable<any>((observer: Observer<any>) => {
// ... calling the service, obtaining a promise
const authenticationPromise = ... ;
// This is a promise, it must be converted to an Observable
authenticationPromise
.then(() => {
observer.next('ok');
observer.complete();
})
.catch(err => {
console.log('service error ' + err);
throw new Error('crap');
});
});
}
Setting all the ngrx & ngrx/effect part aside, this authentication method is called upon user request:
(redux stuff).(({ payload }: actions.LoginRequestAction) =>
context.authService.authenticate(payload)
.pipe(
map(() => new GenericSuccessAction(...))
// Even though a throw statement happened in the authenticate method, this is never reached:
catchError((err: Error) => {
console.log('[debug] error caught: ', err);
return of(new actions.LoginFailureAction());
}),
)
)
As stated here, catchError is used to:
Gracefully handle errors in an observable sequence.
First of all, you are basically handling the error in your promise by catching the error in your promise. Throwing the error in the promise doesn't return an observable that emits an error.
You can:
Convert your promise into an observable and don't use .catch at all.
Return the error as an observable with rxjs' throwError(err)
Either way, the way you create your observable is questionable.
This is a much better and concise way to handle promises in rxjs:
from(authenticationPromise)
.pipe(
map(() => 'ok')
)
I'm trying to write a test for the Jasmine Test Framework which expects an error. At the moment I'm using a Jasmine Node.js integration from GitHub.
In my Node.js module I have the following code:
throw new Error("Parsing is not possible");
Now I try to write a test which expects this error:
describe('my suite...', function() {
[..]
it('should not parse foo', function() {
[..]
expect(parser.parse(raw)).toThrow(new Error("Parsing is not possible"));
});
});
I tried also Error() and some other variants and just can't figure out how to make it work.
Try using an anonymous function instead:
expect( function(){ parser.parse(raw); } ).toThrow(new Error("Parsing is not possible"));
you should be passing a function into the expect(...) call. Your incorrect code:
// incorrect:
expect(parser.parse(raw)).toThrow(new Error("Parsing is not possible"));
is trying to actually call parser.parse(raw) in an attempt to pass the result into expect(...),
You are using:
expect(fn).toThrow(e)
But if you'll have a look on the function comment (expected is string):
294 /**
295 * Matcher that checks that the expected exception was thrown by the actual.
296 *
297 * #param {String} expected
298 */
299 jasmine.Matchers.prototype.toThrow = function(expected) {
I suppose you should probably write it like this (using lambda - anonymous function):
expect(function() { parser.parse(raw); } ).toThrow("Parsing is not possible");
This is confirmed in the following example:
expect(function () {throw new Error("Parsing is not possible")}).toThrow("Parsing is not possible");
Douglas Crockford strongly recommends this approach, instead of using "throw new Error()" (prototyping way):
throw {
name: "Error",
message: "Parsing is not possible"
}
As mentioned previously, a function needs to be passed to toThrow as it is the function you're describing in your test: "I expect this function to throw x"
expect(() => parser.parse(raw))
.toThrow(new Error('Parsing is not possible'));
If using Jasmine-Matchers you can also use one of the following when they suit the situation;
// I just want to know that an error was
// thrown and nothing more about it
expect(() => parser.parse(raw))
.toThrowAnyError();
or
// I just want to know that an error of
// a given type was thrown and nothing more
expect(() => parser.parse(raw))
.toThrowErrorOfType(TypeError);
A more elegant solution than creating an anonymous function whose sole purpose is to wrap another, is to use ES5's bind function. The bind function creates a new function that, when called, has its this keyword set to the provided value, with a given sequence of arguments preceding any provided when the new function is called.
Instead of:
expect(function () { parser.parse(raw, config); } ).toThrow("Parsing is not possible");
Consider:
expect(parser.parse.bind(parser, raw, config)).toThrow("Parsing is not possible");
The bind syntax allows you to test functions with different this values, and in my opinion makes the test more readable. See also:
Does Jasmine's toThrow matcher require the argument to be wrapped in an anonymous function?
I replace Jasmine's toThrow matcher with the following, which lets you match on the exception's name property or its message property. For me this makes tests easier to write and less brittle, as I can do the following:
throw {
name: "NoActionProvided",
message: "Please specify an 'action' property when configuring the action map."
}
and then test with the following:
expect (function () {
.. do something
}).toThrow ("NoActionProvided");
This lets me tweak the exception message later without breaking tests, when the important thing is that it threw the expected type of exception.
This is the replacement for toThrow that allows this:
jasmine.Matchers.prototype.toThrow = function(expected) {
var result = false;
var exception;
if (typeof this.actual != 'function') {
throw new Error('Actual is not a function');
}
try {
this.actual();
} catch (e) {
exception = e;
}
if (exception) {
result = (expected === jasmine.undefined || this.env.equals_(exception.message || exception, expected.message || expected) || this.env.equals_(exception.name, expected));
}
var not = this.isNot ? "not " : "";
this.message = function() {
if (exception && (expected === jasmine.undefined || !this.env.equals_(exception.message || exception, expected.message || expected))) {
return ["Expected function " + not + "to throw", expected ? expected.name || expected.message || expected : " an exception", ", but it threw", exception.name || exception.message || exception].join(' ');
} else {
return "Expected function to throw an exception.";
}
};
return result;
};
I know that is more code, but you can also do:
try
Do something
#fail Error("should send a Exception")
catch e
expect(e.name).toBe "BLA_ERROR"
expect(e.message).toBe 'Message'
In my case, the function throwing an error was async, so I followed this:
await expectAsync(asyncFunction()).toBeRejected();
await expectAsync(asyncFunction()).toBeRejectedWithError(...);
For CoffeeScript lovers:
expect( => someMethodCall(arg1, arg2)).toThrow()
For me, the posted solution didn't work and it kept on throwing this error:
Error: Expected function to throw an exception.
I later realised that the function which I was expecting to throw an error was an async function and was expecting the promise to be rejected and then throw an error and that's what I was doing in my code:
throw new Error('REQUEST ID NOT FOUND');
And that’s what I did in my test and it worked:
it('Test should throw error if request not found', willResolve(() => {
const promise = service.getRequestStatus('request-id');
return expectToReject(promise).then((err) => {
expect(err.message).toEqual('REQUEST NOT FOUND');
});
}));
it('it should fail', async () => {
expect.assertions(1);
try {
await testInstance.doSomething();
}
catch (ex) {
expect(ex).toBeInstanceOf(MyCustomError);
}
});