Angular 9/rxjs: How to handle an error thrown inside switchMap? - rxjs

I'm using Angular (9) powered Bootstrap (6.1.0) TypeAhead and defining its search function like so:
search = (text$: Observable<string>) => {
return text$.pipe(
debounceTime(200),
distinctUntilChanged(),
// switchMap allows returning an observable rather than maps array
switchMap((searchText) => {
if (!searchText || searchText.trim().length == 0) {
// when the user erases the searchText
this.dealerRepUserID = 0;
this.dealerRepChanging.emit(this.dealerRepUserID);
return EMPTY;
}
else if (this.dealerID == this.hostOrganizationID) {
// get a list of host reps
return this.myService.getHostRepsAutoComplete(searchText, this.includeInactive);
} else {
// get a list of dealer reps
return this.myService.getDealerReps(this.dealerID, searchText);
}
})
);
}
The function must return an Observable. How do I catch an error thrown inside the switchMap?

Have you try the catchError
import { catchError } from 'rxjs/operators';
return text$.pipe(
debounceTime(200),
distinctUntilChanged(),
// switchMap allows returning an observable rather than maps array
switchMap((searchText) => {
if (!searchText || searchText.trim().length == 0) {
// when the user erases the searchText
this.dealerRepUserID = 0;
this.dealerRepChanging.emit(this.dealerRepUserID);
return EMPTY;
}
else if (this.dealerID == this.hostOrganizationID) {
// get a list of host reps
return this.myService.getHostRepsAutoComplete(searchText, this.includeInactive).pipe(catchError(error => of());
} else {
// get a list of dealer reps
return this.myService.getDealerReps(this.dealerID, searchText).pipe(catchError(error => of());
}
})
);
Here is my app effect
public loadDataPerformance$: Observable<Action> = createEffect(() => {
return this.actions$.pipe(
ofType(RiskProfileActions.loadDataPerformance),
withLatestFrom(
this.store$.select(fromRoot.getAnalyticsFilterSelectedOptions),
this.store$.pipe(select(fromFactoryPerformance.getFactoryId))
),
switchMap(([{ recordDate }, filters, factoryId]) =>
this.riskProfileApiService.getDataPerformanceData(filters, factoryId, recordDate).pipe(
map((riskDataPerformanceData: PerformanceDataModel) =>
RiskProfileActions.loadRiskScoreBreakdownPerformanceSuccess(riskDataPerformanceData)
),
catchError(error => of(RiskProfileActions.loadRiskScoreBreakdownPerformanceFail(error)))
)
)
);
});

The switchMap by itself won't throw any error, the thing that might do something unexpected are the returned observables this.myService.getHostRepsAutoComplete and this.myService.getDealerReps. A tricky moment with the catching errors is that whenever there is an error the observable which is throwing the error is being killed.
For example
observable$.pipe(
switchMap(() => observable2$),
catchError(() => doSomethingFunction())
).subscribe()
observable$ will be completed once there is an error, that will complete your search stream and you will get no more data after the error.
As Phat Tran Ky showed in his example the handling of errors should happen inside the new streams in the switchMap operator
observable$.pipe(
switchMap(() => observable2$.pipe(catchError(() => doSomethingFunction())),
)
).subscribe()
By doing so whenever there is an error thrown from inside it will kill the inner observable (observable2$) but won't kill the outer subscriptions on the outer observable observable$
A further enhancement that you can do in order to handle your errors in one spot might be to merge the inner observable in one, for example, something like
observable$.pipe(
switchMap(() => {
return merge(
observable1$.pipe(filter(() => ${your if else condition for case 1})),
observable2$.pipe(filter(() => ${your if else condition for case 2})),
observable3$.pipe(filter(() => ${your if else condition for case 3})),
).pipe(catchError((error) => yourErrorHandlerFunction(error)))
})),
)
).subscribe()

Related

Angular 11 how to make one of the http request in higher order mapping conditionally

I want to use a better solution if exists so it will make the following simpler.
If this.projectguid is empty then want to use the switchMap call otherwise the other one.
Can anyone suggest me a better solution for this?
getWorksheet(): Observable<Worksheet | number> {
if(this.projectsGuid === '') {
return this.apiService.get('projects/latest')
.pipe(
switchMap((res: {id: string, name: string}) => {
this.projectsGuid = res.id
let getUrl = `projects/${this.projectsGuid}/questionnaires/${this.questionnaireId}/worksheets/latest`;
return this.apiService.get<Worksheet>(getUrl).pipe(
catchError((err) => {
return of(err.status);
})
);
})
)
} else {
let getUrl = `projects/${this.projectsGuid}/questionnaires/${this.questionnaireId}/worksheets/latest`;
return this.apiService.get<Worksheet>(getUrl).pipe(
catchError((err) => {
return of(err.status);
})
);
}
}
You could define a projectGuid$ observable that emits the this.projectsGuid value if not empty, and otherwise emits the result of the http call (mapped to just the id):
const projectGuid$ = this.projectsGuid !== ''
? of(this.projectsGuid)
: this.apiService.get('projects/latest').pipe(
map(({id}) => id),
tap(guid => this.projectsGuid = guid)
);
Then you can pipe the projectGuid to the call to fetch the worksheet:
return projectGuid$.pipe(
map(guid => `projects/${guid}/questionnaires/${this.questionnaireId}/worksheets/latest`),
switchMap(url => this.apiService.get<Worksheet>(url).pipe(
catchError(err => of(err.status))
))
);

Keeping error information and the outer observable alive

To ensure an error doesn't complete the outer observable, a common rxjs effects pattern I've adopted is:
public saySomething$: Observable<Action> = createEffect(() => {
return this.actions.pipe(
ofType<AppActions.SaySomething>(AppActions.SAY_SOMETHING),
// Switch to the result of the inner observable.
switchMap((action) => {
// This service could fail.
return this.service.saySomething(action.payload).pipe(
// Return `null` to keep the outer observable alive!
catchError((error) => {
// What can I do with error here?
return of(null);
})
)
}),
// The result could be null because something could go wrong.
tap((result: Result | null) => {
if (result) {
// Do something with the result!
}
}),
// Update the store state.
map((result: Result | null) => {
if (result) {
return new AppActions.SaySomethingSuccess(result);
}
// It would be nice if I had access the **error** here.
return new AppActions.SaySomethingFail();
}));
});
Notice that I'm using catchError on the inner observable to keep the outer observable alive if the underlying network call fails (service.saySomething(action.payload)):
catchError((error) => {
// What can I do with error here?
return of(null);
})
The subsequent tap and map operators accommodate this in their signatures by allowing null, i.e. (result: Result | null). However, I lose the error information. Ultimately when the final map method returns new AppActions.SaySomethingFail(); I have lost any information about the error.
How can I keep the error information throughout the pipe rather than losing it at the point it's caught?
As suggested in comments you should use Type guard function
Unfortunately I can't run typescript in snippet so I commented types
const { of, throwError, operators: {
switchMap,
tap,
map,
catchError
}
} = rxjs;
const actions = of({payload: 'data'});
const service = {
saySomething: () => throwError(new Error('test'))
}
const AppActions = {
}
AppActions.SaySomethingSuccess = function () {
}
AppActions.SaySomethingFail = function() {
}
/* Type guard */
function isError(value/*: Result | Error*/)/* value is Error*/ {
return value instanceof Error;
}
const observable = actions.pipe(
switchMap((action) => {
return service.saySomething(action.payload).pipe(
catchError((error) => {
return of(error);
})
)
}),
tap((result/*: Result | Error*/) => {
if (isError(result)) {
console.log('tap error')
return;
}
console.log('tap result');
}),
map((result/*: Result | Error*/) => {
if (isError(result)) {
console.log('map error')
return new AppActions.SaySomethingFail();
}
console.log('map result');
return new AppActions.SaySomethingSuccess(result);
}));
observable.subscribe(_ => {
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.5/rxjs.umd.js"></script>
I wouldn't try to keep the error information throughout the pipe. Instead you should separate your success pipeline (tap, map) from your error pipeline (catchError) by adding all operators to the observable whose result they should actually work with, i.e. your inner observable.
public saySomething$: Observable<Action> = createEffect(() => {
return this.actions.pipe(
ofType<AppActions.SaySomething>(AppActions.SAY_SOMETHING),
switchMap((action) => this.service.saySomething(action.payload).pipe(
tap((result: Result) => {
// Do something with the result!
}),
// Update the store state.
map((result: Result) => {
return new AppActions.SaySomethingSuccess(result);
}),
catchError((error) => {
// I can access the **error** here.
return of(new AppActions.SaySomethingFail());
})
)),
);
});
This way tap and map will only be executed on success results from this.service.saySomething. Move all your error side effects and error mapping into catchError.

RXJS switchmap + tap like operator

I have a stream of files and I want to fill additional information about it, but I would like to present the currently obtained data to the user, as it is all that is initially visible anyway.
I want observable that:
Get cancelled on new emission (like switchMap)
Does not wait for the observable to finish before emitting (like tap)
What I have currently is awaiting the result, before emitting the files.
Set-up & current try itteration:
this.pagedFLFiles = fileService.getFiles().pipe(
switchMap(response => concat(
of(response),
fileService.getAdditionalInfo(response.items).pipe(
switchMap(() => EMPTY),
),
)),
shareReplay(1),
);
fileService.getAdditionalInfo(response.items) - it is modifing the data
getAdditionalInfo(files: FLFile[]): Observable<FLFile[]> {
return this.api.getWithToken(token => {
return { path: `v5/user/${token}/files/${files.map(file => file.id).join(',')}}/facilities` };
}).pipe(
map(information => {
files.forEach(file => {
const info = information[file.id];
(Object.entries(info) as [keyof typeof info, any][]).forEach(([key, value]) => {
file[key] = value;
});
});
return files;
}),
);
}
Use merge instead of concat.
Concat waits for both observables, of(reponse) and getAdditionalInfo, before emitting a value.
Merge emits each time one of its observables emits.
Example:
getFiles will emit each second for 3 seconds
getAdditionalInfo will be cancelled 2 times (because it runs longer than 1 seond), and therefore will only modify the last emitted files array
import { merge, EMPTY, timer, of, interval } from 'rxjs';
import { finalize, switchMap, map, take, shareReplay } from 'rxjs/operators';
const fileService = {
getFiles: () => interval(1000).pipe(
take(3),
map(x => {
const items = [0, 1, 2].map(i => { return { 'info1': i }; })
return { 'index': x, 'items': items };
})
),
getAdditionalInfo: (files) => {
let wasModified = false;
return timer(2000).pipe(
map(information => {
files.forEach(file => {
file['info2'] = 'information' + files.length;
});
console.log('getAdditionalInfo: modified data');
wasModified = true;
return files;
}),
finalize(() => {
if (!wasModified) {
console.log('getAdditionalInfo: cancelled');
}
})
);
}
}
const pagedFLFiles = fileService.getFiles().pipe(
switchMap(response => {
return merge(
of(response),
fileService.getAdditionalInfo(response.items).pipe(
switchMap(() => EMPTY),
));
}
),
shareReplay(1),
);
pagedFLFiles.subscribe(x => {
console.log('immediate', x.index);
});
Stackblitz

RxJS: deferred inner-observables still run even when the outer-observable has no subscription left

I just realized that inner-observables (like those defined in a mergeMap or switchMap operator) do not "stop" even when the outer-observable has no subscription left.
For a better example, let's show some code:
const {
Subject,
of: obsOf,
concat: obsConcat,
defer,
} = require("rxjs");
const {
finalize,
mergeMap,
tap,
takeUntil,
} = require("rxjs/operators");
const subject = new Subject();
obsOf(null).pipe(
mergeMap(() =>
obsConcat(
defer(() => {
console.log("side-effect 1");
return obsOf(1);
}),
defer(() => {
console.log("side-effect 2");
return obsOf(2);
}),
defer(() => {
console.log("side-effect 3");
return obsOf(3);
})
)
),
finalize(() => {
console.log("finalized");
})
)
.pipe(
takeUntil(subject),
tap((i) => {
if (i === 2) {
subject.next();
}
})
).subscribe(
(i) => { console.log("next", i); },
(e) => { console.log("error", e); },
() => { console.log("complete"); },
);
// Ouput:
// > side-effect 1
// > next 1
// > side-effect 2
// > complete
// > finalized
// > side-effect 3
The fact that the side-effect 3 line is logged is weird since the outer observable already called finalize.
As all those side-effects are in a defer, they could perfectly be avoided after unsubscription. From my point-of-view, those side-effects provide no value at all.
Any idea why RxJS still execute those ?
This is unfortunately by design (as of RxJS 6) - concat will buffer the observables and will subscribe to each buffered one even after you unsubscribe (if the subscription is closed it will subscribe and immediately unsubscribe).
You have to prevent the observables from getting buffered...
obsOf(null).pipe(
mergeMap(() => obsOf(
defer(() => {
console.log("side-effect 1");
return obsOf(1);
}),
defer(() => {
console.log("side-effect 2");
return obsOf(2);
}),
defer(() => {
console.log("side-effect 3");
return obsOf(3);
})
)),
concatAll(),
finalize(() => {
console.log("finalized");
}),
takeUntil(subject),
tap((i) => {
if (i === 2) {
subject.next();
}
})
).subscribe(
(i) => { console.log("next", i); },
(e) => { console.log("error", e); },
() => { console.log("complete"); },
);
One could think the code above works, but only until you delay one of the observables. Replace obsOf(1) with timer(100).pipe(mapTo(1)); and behavior is exactly the same.
The only workaround is to make sure you are not buffering anything (mean don't use concat* operators) or limit observable production some other way (use separate Subject and control the production manually).

RXJS throw error in a fromPromise does not hit 'catchError' piped method

Can someone explain why this promise (runQuery) , which fails into the 'catch' and throws an ErrorObservable, does not get caught in the 'catchError' method, but goes into 'map' method.
( I tried both _throw/ErrorObservable approach, same result )
import { _throw } from 'rxjs/observable/throw';
import { ErrorObservable } from 'rxjs/observable/ErrorObservable';
const runQuery = ( Promise that rejects )
const source$ = fromPromise(
runQuery({ d: 'g' })
.catch(err => {
return new ErrorObservable(err);
//return _throw(err); -- same as above
})
)
.pipe(
map((response: any) => {
//ENTERS HERE as response.error
}),
catchError(e => {
//DOES NOT ENTER
})
);
You are handling your own error in the promise and let it return an ErrorObservable. By doing so you made the promise succeed (although with an error like return value). fromPromise will convert this in an emission instead of error.
Remove the catch case in the fromPromise and it should hit the Observable.catchError
I made a live example to prove my point.
There is no need to handle the promise (when it is still a promise), so this far from recommendable.
.then(data => {
return data;
})
.catch(err => {
return new ErrorObservable(err);
//return _throw(err); -- same as above
}))
Let observables take control of rejections
const source$ = fromPromise(runQuery())
.pipe(
map((response: any) => {
//Make here some transformations
return response;
}),
catchError(e => {
return _throw(e);
}
));
If you want to change the error object or do a console.log() inside the catch in the promise before the observable receives the error you can throw the error:
.then(data => {
return data;
})
.catch(err => {
console.error("Error", err );
err.code = 110; // you may want to change something before passing to observable
throw err;
}))
If you use return error the error or catch pipe of the observable won't work, but using throw it works.

Resources