I have implemented some error handling code in my main function as below. It uses the catch operator to filter and report on errors in one stream and ignore them in another. This allows me to know about and report on errors that occur with requests whilst not failing the entire stream so that subsequent requests can continue.
For reasons that might not be apparent in the code snippets below I am impementing a custom driver to request and handle data. I'm not using the cycle http driver.
Here's my code that successfully reports an error:
function main(sources) {
// Catch driver errors so they can be logged
const error$ = sources.CustomDriver
.map(x => x.catch(e => Rx.Observable.just(e)))
.flatMap(p => p)
// Filter out the errors to deal with requests that did not fail
const data$ = sources.CustomDriver
.map(x => x.catch(e => Rx.Observable.empty()))
.flatMap(p => p)
return {
CustomDriver: Rx.Observable.just('initial event'),
Log: data$,
Error: error$
}
}
Cycle.run(main, {
CustomDriver: makeCustomDriver(),
Log: msg$ => { msg$.subscribe(
msg => console.log('LOG: ', msg),
err => console.log('problem with Log driver: ', err),
() => console.log('Log Completed')
) },
Error: msg$ => { msg$.subscribe(
e => console.log('ERR: ', e),
err => console.log('problem with Error driver:', err),
() => console.log('Error Completed')
) }
})
function makeCustomDriver() {
return function customDriver(requests$) {
return requests$
.map(request => Rx.Observable.fromPromise(makeFailedRequest()))
}
}
function makeFailedRequest() {
console.log('some API request')
return Promise.reject('error')
}
The output is as follows:
some API request
some API request
Log Completed
ERR: error
Error Completed
On the plus side the error is reported. However, the API request is actually made twice, which is not what I expected to happen initially.
After learning some more RxJS and getting a better understanding of Hot and Cold observables I realised that I was creating two subscriptions to the CustomDriver stream (one for error$ and one for data$) and because the CustomDriver Observable was cold it would repeat the Observable.just for each subscriber.
So I tried to make my CustomDriver Observavble hot with share:
function makeCustomDriver() {
return function customDriver(requests$) {
return requests$
.map(request => Rx.Observable.fromPromise(makeFailedRequest()))
.share()
}
}
With that change, the output is as follows:
some API request
Error Completed
Log Completed
So I managed to get rid of the duplicate request but the error was swallowed up in the process.
What is happening with share that causes the errors to be lost and how can I avoid duplicate requests without losing errors?
.shareReplay(1) appears to give the desired result.
There is a factory for making custom drivers of that kind that you want (from Promises) https://github.com/whitecolor/cycle-async-driver it includes, helpers for dealing with errors (success and failure).
You can created drivers just like that:
import {makeAsyncDriver} from 'cycle-async-driver'
customDriver = makeAsyncDriver(
(request) => requestHanderThatReturnsPromise(reques)
)
Related
I am building an application using RxJS and one of the interesting problems I have come across is how I can implement an operator that will catch errors only when they are unhandled by the rest of the pipeline ahead of it. I'll call my operator catchUnhandledError for now.
The catchError operator is loosely similar (non stream) to
try {
// stream
} catch (err) {
// catchError handler
// new stream
}
What I am trying to implement resembles the following
try {
// stream
} catch (err) {
try {
// original stream
} catch (err2) {
// catchUnhandledError handler
// new stream
}
}
The key takeaway here is that the new operator only catches errors that other operators do not catch further down the pipeline.
I appreciate that piped operators essentially wrap observables similar to how middleware works in popular application pipelines which means "pushing the pipe to the end" is nonsensical.
My stream (simplified) is created as follows.
combineLatest([ page, filter ]).pipe(
switchMap(([ page, { search, order }]) =>
apiQuery(search, order, page).pipe(
map(/* combine response with filters */),
catchError(/* ONLY handle http status 422 Unprocessable Entity for validation */),
catchError(/* ONLY handle http status 409 Conflict for version conflicts *)
)
)
);
Where api.query returns:
function errorHandler(errorMessage?: string) {
return <T>(source: Observable<T>): Observable<T> =>
source.pipe(
/* replace with new catchUnhandledError operator */
catchError(() => {
/* More complex code than this to log the error */
alert('unhandled error');
return EMPTY;
})
);
}
function apiQuery(search: string, order: string, page: number) {
/* uses parameters to generate ajax payload */
return ajax({
url: 'url',
method: 'GET'
}).pipe(
map(r => r.response as T),
errorHandler('An error message')
);
}
The problem is the validation / request specific error handlers never get called because the generic errorHandler takes precedence. A couple of work arounds I will have to use if this is not possible:
Pass everything in the pipe as a parameter / callback (convoluted)
Map success and errors to a single object then check .success (convoluted)
Copy and paste the generic errorHandler to every single place I call my api (duplicated)
Does anyone have any ideas that will prevent me from having to have convoluted or duplicated code?
The first thing that comes to mind is to create a custom operator that will be given a callback function with which you can determine whether the catchError should handle this or should pass it along further in the stream.
const catchUnhandledError = (shouldHandle: (err: any) => boolean) =>
(source: Observable<any>) => defer(
() => source.pipe(
catchError(err => shouldHandle(err) ? of('handled') : throwError(err) /* pass along the error */)
)
)
I am developing an app that uses promises to communicate with an remote API. This app needs to be able to work offline seamlessly, so I need to handle network errors. Since I can define some default data upfront that is good enough to keep the app functioning. My approach is to catch the error and return a new promise loaded with the default data:
API.js
function getDataFromAPI(id) {
return axios.get(`${BASE_URL}/${id}`)
.then(response => response.data)
.catch((error) => {
// Only return fake data in cases of connection issues
if (error.message == 'Network error') {
const fakeResponse = {myDefaultData: 'default data all over the place'};
// receiving function expects data in promise-form
return Promise.resolve(fakeResponse);
}
});
}
Action.js using the API
needSomeData = () => {
api.getDataFromAPI().then((response) => {
// Data is processed/used here
}));
The code sample works but I am not sure if this is a good/clean approach? Would it be better to handle this in a service worker? Or should I use an entirely different way to approach the issue?
so you can clean it a little bit more.
since any return from .catch consider the value of the next resolved promise. you do not need to return Promise.resolve(value) return value are enough
function getDataFromAPI(id) {
return axios.get(`${BASE_URL}/${id}`)
.then(response => response.data)
.catch((error) => {
// Only return fake data in cases of connection issues
if (error.message == 'Network error') {
return {
myDefaultData: 'default data all over the place'
};
else {
return 'return something or throw new exception'
}
});
}
So for whom that want to know exactly how Promise algorithm behave
Promises/A+ specification
In fact I find It very interesting
This question is more about the best practice for error handling within Angular 4.
After the reading the Angular 4 documentation I've found that all error handling should be done in the Services and the Components don't need to worry about that.
Currently I handle the error within the my subscribe call to my observable.
logIn(){
this._userService.logIn({"email":this.loginForm.value.email,"password": this.loginForm.value.password})
.subscribe(
data => {//handle data here},
(err: HttpErrorResponse) => {
if (err.error instanceof Error) {
// A client-side or network error occurred. Handle it accordingly.
console.log('An error occurred:', err.error.message);
} else {
// The backend returned an unsuccessful response code.
// The response body may contain clues as to what went wrong,
console.log(`Backend returned code ${err.status}, body was: ${err.error.message}`);
}
}
)}
Im struggling to handle errors within Data Service. Im looking for some professional advice on how I can handle errors properly.
My Data Service:
logIn(body): Observable<any>{
return this._http.post('http://localhost:3000/api/login', body)
.do(data => console.log('Login: ' + JSON.stringify(data)))
}
You can use catch and throw operator to preprocess the error (there is also finally operator btw):
logIn(body): Observable<any> {
return this._http
.post('http://localhost:3000/api/login', body)
.do(data => console.log('Login: ' + JSON.stringify(data)))
.catch(err => {
// do whatever you want when error occurres
console.log(err);
// re-throw error so you can catch it when subscribing, fallback to generic error code
return Observable.throw(err.message.toUpperCase() || 'API_ERROR');
});
}
You will need to import those first to use them.
Sometimes it can be error that you want to ignore, then just return Observable.of(false); instead of the Observable.throw().
You should do this only for error pre-processing. You will still need to catch it later in the template (therefore we need to re-throw it), if you want to adjust the template based on it. But the code in component should ideally just catch the error and assign it to template (or show alert or whatever).
logIn() {
this._userService.logIn({...})
.subscribe(
data => this.data = data, // process data
(err: string) => this.error = err // process error
)
}
I can't understand why onCompletion is not called when an error occures (e.g http 401/404). Shouldn't be the expected behaviour?
this.http.get('http://localhost/xxx')
.map((res) => res.json())
.subscribe(
(res) => {
console.log('onSuccess');
console.log(res);
},
(err) => {
console.log('onError');
console.log(err);
},
() => {
console.log('onCompletion');
});
In an Observable Execution, zero to infinite Next notifications may be delivered. If either an Error or Complete notification is delivered, then nothing else can be delivered afterwards.
http://reactivex.io/rxjs/manual/overview.html#executing-observables
There are 3 types of events:
next
error
complete
And they occur according to this (RegExp) scheme: n*(e|c)
The documentation states this too, as Julia points out.
I have a mobile app I'm building and right now I'm working on authentication. Before I hit my home page I need to hit a variety of endpoints on an API I've built before I can display data to the user.
All the endpoints are returning the correct data when tested in Postman, however I'm getting a null value in my second async call when I utilize it in my app.
I'm sure it has something to do with the order in which these calls are made, so I was just looking for some help as to how I can properly wait for one call to finish before starting another one.
public login() {
this.showLoading();
this.userService.getUserIdFromUserName(this.registerCredentials.username) // WORKS
.subscribe(
res => {
console.log(res);
localStorage.setItem("UserId", res.toString());
},
err => {
console.log(err);
});
this.userService.getEmployeeIdFromUserId(localStorage.getItem("UserId")) // THIS RETURNS NULL
.subscribe(
res => {
console.log(res);
localStorage.setItem("EmployeeId", res.toString());
},
err => {
console.log(err);
});
this.authService.login(this.registerCredentials)
.subscribe(
data => {
this.loading.dismissAll();
console.log('User logged in successfully! ', data);
this.nav.push(TabsPage);
localStorage.setItem("Username", this.registerCredentials.username);
localStorage.setItem("isLoggedIn", "true");
},
error => {
this.loading.dismissAll();
this.showAlert("Uh oh!", "Something went wrong. Please re-enter your login credentials or check your connection.");
console.log(error);
});
}
Your original code has a bug that leads to this error. You have three calls in your code which I will call A), B), and C):
A) this.userService.getUserIdFromUserName(this.registerCredentials.username) // WORKS
B) this.userService.getEmployeeIdFromUserId(localStorage.getItem("UserId")) // THIS RETURNS NULL
C) this.authService.login(this.registerCredentials)
What you need to understand about RXJS is the difference between a cold Observable (which represents all information required to start an async operation) and a hot Observable (which is an Observable with the async operation already started).
The three calls A), B) and C) merely build cold observables which are started the moment you call .subscribe() on them. So by the time B) is built, A) is already started but has not completed yet. So the call to localStorage.getItem("UserId") will return null, because A) has not yet invoked its subscriber's next callback.
So what you want to do is for B) to wait on A). Also instead of stuffing something into global state (localStorage) it's probably better to flow the result from A) through to B). Enter the .mergeMap() operator:
this.userService.getUserIdFromUserName(this.registerCredentials.username) // WORKS
.map(res => res.toString())
.do(userId => localStorage.setItem("UserId", userId)) // cleanly separate side-effects into .do() calls
.mergeMap(userId => this.userService.getEmployeeIdFromUserId(userId))
.map(res => res.toString())
.do(employeeId => localStorage.setItem("EmployeeId", employeeId))
.subscribe(
employeeId => {
console.log(employeeId);
},
err => {
console.log(err);
});
The nice thing about rxjs is that error handling just works all the way through your Observable chain.
If you can execute C) in parallel, have a look at .forkJoin().
Finally, if you need a hands on explanation of .mergeMap() have a look at this answer: SwitchMap vs MergeMap in the #ngrx example
This should work.Don't forget import 'rxjs/Rx'
this.userService.getUserIdFromUserName(this.registerCredentials.username)
.map(res => res.toString())
.do(userId => {
console.log(res);
localStorage.setItem("UserId", userId);
})
.flatMap(userId => {
return this.userService.getEmployeeIdFromUserId(userId);
})
.do(res => {
console.log(res);
localStorage.setItem("EmployeeId", res.toString());
})