Keeping error information and the outer observable alive - rxjs

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.

Related

.pipe(takeUntil) is listening when it is not supposed to

We are using .pipe(takeUntil) in the logincomponent.ts. What I need is, it should get destroyed after successful log in and the user is on the landing page. However, the below snippet is being called even when the user is trying to do other activity and hitting submit on the landing page should load different page but the result of submit button is being overridden and taken back to the landing page.
enter code hereforkJoin({
flag: this.auth
.getEnvironmentSettings('featureEnableQubeScan')
.pipe(take(1)),
prefs: this.auth.preferences.pipe(take(1)),
}).subscribe(
(result: any) => {
this.qubeScanEnabled = result.flag.featureEnableQubeScan;
this.userPrefs = result.prefs;
// check to see if we're authed (but don't keep listening)
this.auth.authed
.pipe(takeUntilComponentDestroyed(this))
.subscribe((payload: IJwtPayload) => {
if (payload) {
this.auth.accountO
.pipe(takeUntilComponentDestroyed(this))
.subscribe((account: IAccount) => {
if (this.returnUrl) {
this.router.navigateByUrl(this.returnUrl);
} else {
this.router.navigate(['dashboard']);
}
}
}
}
}
);
ngOnDestroy() {}
Custom Code:
export function takeUntilComponentDestroyed(component: OnDestroy) {
const componentDestroyed = (comp: OnDestroy) => {
const oldNgOnDestroy = comp.ngOnDestroy;
const destroyed$ = new ReplaySubject<void>(1);
comp.ngOnDestroy = () => {
oldNgOnDestroy.apply(comp);
destroyed$.next(undefined);
destroyed$.complete();
};
return destroyed$;
};
return pipe(
takeUntil(componentDestroyed(component))
);
}
Please let me know what I am doing wrong.
Versions:
rxjs: 6.5.5
Angular:10.0.8
Thanks
I've done a first pass at creating a stream that doesn't nest subscriptions and continues to have the same semantics. The major difference is that I can move takeUntilComponentDestroyed to the end of the stream and lets the unsubscibes filter backup the chain. (It's a bit cleaner and you don't run the same code twice every time through)
It's a matter of taste, but flattening operators are a bit easier to follow for many.
enter code hereforkJoin({
flag: this.auth
.getEnvironmentSettings('featureEnableQubeScan')
.pipe(take(1)),
prefs: this.auth.preferences.pipe(take(1)),
}).pipe(
tap((result: any) => {
this.qubeScanEnabled = result.flag.featureEnableQubeScan;
this.userPrefs = result.prefs;
}),
mergeMap((result: any) => this.auth.authed),
filter((payload: IJwtPayload) => payload != null),
mergeMap((payload: IJwtPayload) => this.auth.accountO),
takeUntilComponentDestroyed(this)
).subscribe((account: IAccount) => {
if (this.returnUrl) {
this.router.navigateByUrl(this.returnUrl);
} else {
this.router.navigate(['dashboard']);
}
});
This function doesn't create another inner stream (destroyed$). This way is a bit more back to the basics so it should be easier to debug if you're not getting the result you want.
export function takeUntilComponentDestroyed<T>(comp: OnDestroy): MonoTypeOperatorFunction<T> {
return input$ => new Observable(observer => {
const sub = input$.subscribe({
next: val => observer.next(val),
complete: () => observer.complete(),
error: err => observer.error(err)
});
const oldNgOnDestroy = comp.ngOnDestroy;
comp.ngOnDestroy = () => {
oldNgOnDestroy.apply(comp);
sub.unsubscribe();
observer.complete();
};
return { unsubscribe: () => sub.unsubscribe() };
});
}

Combining observable outputs in to one array

I need to make two calls to Firebase (as it doesn't support OR queries) and merge the output into one array at the end to return to the calling service.
I have something that gets pretty close but it outputs a 2D array of arrays (one for each call to Firebase). I've tried a few things and this is the best I can get to. Any help on tidying up the below would be great.
getAllFriends(): Observable<[Friendship[], Friendship[]]> {
const invitesSent = from(this.afAuth.currentUser.then(user => {
return user.uid;
}))
.pipe(
switchMap(
userid => {
return this.db.collection('friendships', ref => ref.where('inviter', '==', userid)).snapshotChanges().pipe(map(actions => {
return actions.map(action => {
const data = new Friendship(action.payload.doc.data());
data.id = action.payload.doc.id;
console.log(data);
return data;
});
}));
}
)
);
const invitesReceived = from(this.afAuth.currentUser.then(user => {
return user.uid;
}))
.pipe(
switchMap(
userid => {
return this.db.collection('friendships', ref => ref.where('invitee', '==', userid)).snapshotChanges().pipe(map(actions => {
return actions.map(action => {
const data = new Friendship(action.payload.doc.data());
data.id = action.payload.doc.id;
console.log(data);
return data;
});
}));
}
)
);
return combineLatest([invitesSent, invitesReceived]);
}
Friendship is just an object with property: value pairs, nothing special.
I have tried then putting a .pipe() after this returned observable but that just stops the subscription firing in the calling service.
What about returning, at the end, something like this
return combineLatest([invitesSent, invitesReceived]).pipe(
map(([frienships_1, friendships_2]) => ([...friedships_1, ...friendships_2]))
)

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.

TypeError: You provided an invalid object where a stream was expected

The following code works. It does an ajax request and then call 2 actions, on at a time:
export const loadThingsEpic = action$ => {
return action$.ofType(LOAD_THINGS)
.mergeMap(({things}) => {
const requestURL = `${AppConfig.serverUrl()}/data/things`;
return ajax.getJSON(requestURL)).map(response => {
return finishLoadingThings(response);
}).map(() => {
return sendNotification('success');
});
})
.catch(e => {
return concat(of(finishLoadingThings({ things: {} })),
of(sendNotification('error')));
});
}}
But this code does not:
export const loadThingsEpic = action$ => {
return action$.ofType(LOAD_THINGS)
.mergeMap(({things}) => {
const requestURL = `${AppConfig.serverUrl()}/data/things`;
return ajax.getJSON(requestURL).switchMap(response => {
return concat(of(finishLoadingThings(response)),
of(sendNotification('success')));
});
})
.catch(e => {
return concat(of(finishLoadingThings({ things: {} })),
of(sendNotification('error')));
});
}
I've replace the map by a switchMap to merge 2 actions together (as seen in many other post). It works in the catch if an exception is thrown. I'm wondering whats wrong with the code. I'm guessing it's because I can't seem to really grasp when to use: map, swicthMap and mergeMap.
sendNotification and finishLoadingthings returns action object:
export function finishLoadingThings(data: any) {
return {
type: FINISH_LOADING_THINGS,
data,
};
}
Thanks!
The code provided as-is appears to work as intended: https://jsbin.com/becapin/edit?js,console I do not receive a "invalid object where stream expected" error when the ajax succeeds or fails.
Are you sure the error is coming from this code?
On a separate note, you might be happy to hear that Observable.of supports an arbitrary number of arguments, each one will be emitted after the other. So instead of this:
.switchMap(response => {
return concat(of(finishLoadingThings(response)),
of(sendNotification('success')));
});
You can just do this:
.switchMap(response => {
return of(
finishLoadingThings(response),
sendNotification('success')
);
});
This would not have caused a bug though, it's just cleaner.
I manage to fix my problem, by doing the switchMap at the same level than the mergeMap. Like this:
export const loadThingsEpic = action$ => {
return action$.ofType(LOAD_THINGS)
.mergeMap(({things}) => {
const requestURL = `${AppConfig.serverUrl()}/data/things`;
return ajax.getJSON(requestURL).switchMap(response => {
return of(response);
});
})
.switchMap((res) => {
return concat(of(finishLoadingThings(res.value)),
of(sendNotification('success')));
})
.catch(e => {
return concat(of(finishLoadingThings({ things: {} })),
of(sendNotification('error')));
});
}
Don't quite get it yet.

Forkjoin with empty (or not) array of observables

I'm trying to detect when all my observables have completed. I have the following Observables:
let observables:any[] = [];
if(valid){
observables.push(new Observable((observer:any) => {
async(()=>{
observer.next();
observer.complete();
})
}))
}
if(confirmed){
observables.push(new Observable((observer:any) => {
async(()=>{
observer.next();
observer.complete();
})
}))
}
Observable.forkJoin(observables).subscribe(
data => {
console.log('all completed');
},
error => {
console.log(error);
}
);
I need to do something whenever all my functions are completed. Forkjoin seems to work when the observables array is not empty. But when the array is empty, it never gets called. How can I solve this?
you are missing the 3rd callback in subscribe. try this:
Rx.Observable.forkJoin([]).subscribe(
val => {
console.log('next');
},
err => {
console.log('err');
},
() => {
console.log('complete')
}
);
forkJoin on empty array completes immediately.
Updated for RxJS 6:
let rep: Observable<any>[] = [];
for (let i = 0; i < areas.length; i++) { // undetermined array length
rep.push(this.httpService.GET('/areas/' + areas[i].name)); // example observable's being pushed to array
}
if (rep !== []) {
forkJoin(rep).subscribe(({
next: value => {
console.log(value)
}
}));
}
Try this:
import { forkJoin, Observable, of } from 'rxjs';
export function forkJoinSafe<T = any>(array: Observable<T>[]): Observable<T[]> {
if (!array.length) {
return of([])
}
return forkJoin<T>(array);
}
You're missing complete callback. You can pass the third argument or pass an observer object instead of 3 arguments to make event checking more readable.
yourObservable.subscribe({
next: value => console.log(value),
error: error => console.log(error),
complete: () => console.log('complete'),
});

Resources