Proper Promise handling success and error callbacks - javascript

I have following service in TypeScript that fetches data from the backend.
As a parameter of function getAllPropertiesByAppId I have a success and error callback.
export class PropertyService implements IPropertyService {
/**
* Get all properties
*/
public getAllPropertiesByAppId(appliactionId: string, success: (properties: Array<IPropertyItem>) => void, error: (error: any) => void): void {
//set up createRequestStr and requestInit
fetch(createRequestStr, requestInit)
.then<IPropertyItem[]>((response: Response) => {
if (response.status===401) {
throw new UnAuthorizedException();
}
return response.json<IPropertyItem[]>();
})
.then((response: IPropertyItem[]) => {
success(response);
})
.catch((reason: any) => {
//error handling
}
});
}
Then I am using this service in my action creator:
initProperties: (appId: string): ActionCreator => (dispatch: Redux.Dispatch, getState: () => IApplicationState) => {
"use strict";
console.log("ShoppingCart initProperties - Request all Property");
var service: IPropertyService = kernel.get<IPropertyService>("IPropertyService");
service.getAllPropertiesByAppId(appId, (properties: Array<IPropertyItem>): void => {
dispatch(new ShoppingCartPropertiesLoaded(appId, properties));
dispatch(new ShoppingCartPropertiesDone(appId, System.Init.Done));
}, (error: any): void => {
console.log("ShoppingCart initProperties - error:" + error);
dispatch(new ShoppingCartPropertiesDone(appId, System.Init.Error));
});
}
So when I call initProperties action creator it calls getAllPropertiesByAppId and when everything is fine I will dispatch actions ShoppingCartPropertiesLoaded and ShoppingCartPropertiesDone.
I have simple component connected to store and the component will throw errors when the render method is executing
export default class TotalPriceList extends React.Component<ITotalPriceListProps, void> {
public render(): JSX.Element {
throw 'SomeError';
}
}
The unhandled exception ends up in the catch statement of fetch.
Have I missed something like how to exit promise correctly or even better how to call function/callback from than statement and exit promise, to avoid catch exception from callback in catch statement of fetch?
Thanks very much for your help

As a parameter of function getAllPropertiesByAppId I have success and error callback.
And that's your actual problem. You should always return a promise from an asynchronous function.
Stop using callback parameters!
Your code should read
/**
* Get all properties
*/
public getAllPropertiesByAppId(appliactionId: string): Promise<IPropertyItem[]> {
//set up createRequestStr and requestInit
return fetch(createRequestStr, requestInit)
// ^^^^^^
.then<IPropertyItem[]>((response: Response) => {
if (response.status===401) {
throw new UnAuthorizedException();
}
return response.json<IPropertyItem[]>();
});
}
This will incidentally solve your problem with unhandled rejections. By not ending the chain but returning the promise, you put the responsibility of handling errors on the caller - as usual. Also the caller is implicitly responsible for anything he does in his promise callbacks - they don't concern the promise-returning method at all.
You'd therefore use
service.getAllPropertiesByAppId(appId).then((properties: Array<IPropertyItem>): void => {
// ^^^^^
dispatch(new ShoppingCartPropertiesLoaded(appId, properties));
dispatch(new ShoppingCartPropertiesDone(appId, System.Init.Done));
}, (error: any): void => {
console.log("ShoppingCart initProperties - error:" + error);
dispatch(new ShoppingCartPropertiesDone(appId, System.Init.Error));
}).catch((reason: any) => {
// error handling for a failed dispatch
});

If you do not want to catch an exception inside a promise chain, you only have to remove the .catch call at the end.
But keep in mind that you will not be able to catch this error with a try {} catch (error) {} block. Instead, it will bubble up to the point where you will receive a unhandledRejection at the top level.
If I understood correctly, you are passing a callback (success) to getAllPropertiesByAppId, which returns a promise that calls the callback; fetch(…).then(success) basically. So what you experience is completely to the defined behaviour of exceptions inside functions enclosed in promises.
You might want to switch to using promises all the way instead of mixing continuation passing style (callbacks) and promises.
Something like (in pseudo-code-js, not ts sorry)
class Service {
getAllPropertiesByAppId (appId) {
return fetch(createRequestStr, requestInit)
.then(response => response.json());
}
};
// …
service.getAllPropertiesByAppId(…)
.then(dispatchAllTheThings)
.catch(error => { console.log(…); dispatch(…) })
Throwing a exception inside a dispatched component will then be caught inside the .catch in the promise chain after the call to dispatchAllTheThings.

Related

JavaScript: differences between async error handling with async/await and then/catch

Just wanted to preemptively say that I am familiar with async/await and promises in JavaScript so no need to link me to some MDN pages for that.
I have a function to fetch user details and display it on the UI.
async function someHttpCall() {
throw 'someHttpCall error'
}
async function fetchUserDetails() {
throw 'fetchUserDetails error'
}
function displayUserDetails(userDetails) {
console.log('userDetails:', userDetails)
}
async function fetchUser() {
try {
const user = await someHttpCall()
try {
const details = await fetchUserDetails(user)
returndisplayUserDetails(details)
} catch (fetchUserDetailsError) {
console.log('fetching user error', fetchUserDetailsError)
}
} catch (someHttpCallError) {
console.log('networking error:', someHttpCallError)
}
}
It first makes HTTP call via someHttpCall and if it succeeds then it proceeds to fetchUserDetails and it that succeeds as well then we display the details on Ui via returndisplayUserDetails.
If someHttpCall failed, we will stop and not make fetchUserDetails call. In other words, we want to separate the error handling for someHttpCall and it’s data handling from fetchUserDetails
The function I wrote is with nested try catch blocks which doesn't scale well if the nesting becomes deep and I was trying to rewrite it for better readability using plain then and catch
This was my first atttempt
function fetchUser2() {
someHttpCall()
.then(
(user) => fetchUserDetails(user),
(someHttpCallError) => {
console.log('networking error:', someHttpCallError)
}
)
.then(
(details) => {
displayUserDetails(details)
}, //
(fetchUserDetailsError) => {
console.log('fetching user error', fetchUserDetailsError)
}
)
}
The problem with this is that the second then will run i.e. displayUserDetails even with someHttpCall failing. To avoid this I had to make the previous .catch blocks throw
so this is the updated version
function fetchUser2() {
someHttpCall()
.then(
(user) => fetchUserDetails(user),
(someHttpCallError) => {
console.log('networking error:', someHttpCallError)
throw someHttpCallError
}
)
.then(
(details) => {
displayUserDetails(details)
}, //
(fetchUserDetailsError) => {
console.log('fetching user error', fetchUserDetailsError)
}
)
}
However now the second catch will get called as a result of the throw. So when the someHttpCall failed, after we handled the someHttpCallError error, we would enter this block (fetchUserDetailsError) => { console.log('fetching user error', fetchUserDetailsError) } which is not good since fetchUserDetails never gets called so we shouldn't need to handle fetchUserDetailsError (I know someHttpCallError became fetchUserDetailsError in this case)
I can add some conditional checks in there to distinguish the two errors but it seems less ideal. So I am wondering how I can improve this by using .then and .catch to achieve the same goal here.
I am wondering how I can improve this by using .then and .catch to achieve the same goal here
You don't get to avoid the nesting if you want to replicate the same behaviour:
function fetchUser2() {
return someHttpCall().then(
(user) => {
return fetchUserDetails(user).then(
(details) => {
return displayUserDetails(details)
},
(fetchUserDetailsError) => {
console.log('fetching user error', fetchUserDetailsError)
}
)
},
(someHttpCallError) => {
console.log('networking error:', someHttpCallError)
throw someHttpCallError
}
)
}
(The exact equivalent to try/catch would use .then(…).catch(…) instead of .then(…, …), but you might not actually want that.)
The function I wrote is [nested] which doesn't scale well if the nesting becomes deep and I was trying to rewrite it for better readability […]
For that, I would recommend to combine await with .catch():
async function fetchUser() {
try {
const user = await someHttpCall().catch(someHttpCallError => {
throw new Error('networking error', {cause: someHttpCallError});
});
const details = await fetchUserDetails(user).catch(fetchUserDetailsError => {
throw new Error('fetching user error', {cause: fetchUserDetailsError});
});
return displayUserDetails(details);
} catch (someError) {
console.log(someError.message, someError.cause);
}
}
(The cause option for Error is still quite new, you might need a polyfill for that)
I can add some conditional checks in there to distinguish the two errors but it seems less ideal.
Actually, that sounds like an ideal situation. That means that you don't have to nest any try / catch blocks which could make you code a lot more readable. This is one of the things that async / await is meant to solve.
A solution could be is to create custom errors by extending the Error interface to be able to determine how and where the error occurs.
class CustomError extends Error {
constructor(name, ...args) {
super(...args)
this.name = name
}
}
Throw your errors within the functions that correspond with the error.
async function someHttpCall() {
throw new CustomError('HttpCallError', 'someHttpCall error');
}
async function fetchUserDetails(user) {
throw new CustomError('UserDetailsError', 'fetchUserDetails error')
}
Now you can control your error flow by checking the name property on the error to differentiate your errors.
async function fetchUser() {
try {
const user = await someHttpCall()
const details = await fetchUserDetails(user)
return displayUserDetails(details)
} catch (error) {
switch(error.name) {
case 'HttpCallError':
console.log('Networking error:', error)
break
case 'UserDetailsError':
console.log('Fetching user error', error)
break
}
}
}
I've been inspired by Rust's Result type (which forces you to handle every potential error along the way).
So what I do is handle exceptions in every individual function, and never allow one to throw, instead returning either an Error (if something went wrong) or the desired return value (if no exception occurred). Here's an example of how I do it (comments included):
TS Playground
If you aren't familiar with TypeScript, you can see the JavaScript-only version of the following code (with no type information) at the TypeScript Playground link above (on the right side of the page).
// This is the code in my exception-handling utility module:
// exception-utils.ts
export type Result <T = void, E extends Error = Error> = T | E;
export function getError (value: unknown): Error {
return value instanceof Error ? value : new Error(String(value));
}
export function isError <T>(value: T): value is T & Error {
return value instanceof Error;
}
export function assertNotError <T>(value: T): asserts value is Exclude<T, Error> {
if (value instanceof Error) throw value;
}
// This is how to use it:
// main.ts
import {assertNotError, getError, isError, type Result} from './exception-utils.ts';
/**
* Returns either Error or string ID,
* but won't throw because it catches exceptions internally
*/
declare function getStringFromAPI1 (): Promise<Result<string>>;
/**
* Requires ID from API1. Returns either Error or final number value,
* but won't throw because it catches exceptions internally
*/
declare function getNumberFromAPI2 (id: string): Promise<Result<number>>;
/**
* Create version of second function with no parameter required:
* Returns either Error or final number value,
* but won't throw because it catches exceptions internally
*
* The previous two functions work just like this, using the utilities
*/
async function fetchValueFromAPI2 (): Promise<Result<number>> {
try {
const id = await getStringFromAPI1(); // Error or string
assertNotError(id); // throws if `id` is an Error
return getNumberFromAPI2(id); // Error or number
}
catch (ex) {
return getError(ex);
}
}
async function doSomethingWithValueFromAPI2 (): Promise<void> {
const value = await fetchValueFromAPI2(); // value is number or Error
if (isError(value)) {
// handle error
}
else console.log(value); // value is number at this point
}

Ignore a rejected fire-and-forget promise in jest

My store's processAction() function calls a private async function in a fire-and-forget manner which then does a fetch. processAction() itself does not handle any error handling, and--in the browser--if the fetch fails, an external library handles any and all uncaught promise rejections.
So, if I mock my fetch to reject, the private function--the effects of which I am testing--will reject. Since I don't have a reference to the promise created by my async function call, I have no way of catching the rejection within the test, but the test fails because there was an unhandled rejection.
How can I tell jest to be okay with this short of calling the private function itself rather than just triggering the action that calls it?
actions.ts
const actions = {
doTheThing() {
dispatch({ type: 'DO_THE_THING' });
},
};
export default actions;
store.ts
import fetch from './fetch';
class Store {
isFetching = false;
// ...
processAction({ type, payload }: { type: string, payload: any }) {
switch (type) {
case 'DO_THE_THING':
this.fetchTheThing();
break;
}
}
private async fetchTheThing() {
try {
this.isFetching = true;
const result = await fetch(myUrl);
// ...
} finally {
this.isFetching = false;
}
}
}
export default new Store();
__mocks__/fetch.ts
let val: any;
interface fetch {
__setVal(value: any): void;
}
export default async function fetch() {
return val;
}
fetch.__setVal = function(value: any) {
val = value;
};
store.test.ts
import actions from './actions';
import store from './store';
const fetch = (require('./fetch') as import('./__mocks__/fetch')).default;
jest.mock('./fetch');
test('it sets/unsets isFetching on failure', async () => {
let rej: () => void;
fetch.__setVal(new Promise((_, reject) => rej = reject));
expect(store.isFetching).toBe(false);
Actions.doTheThing();
await Promise.sleep(); // helper function
expect(store.isFetching).toBe(true);
rej(); // <---- test fails here
await Promise.sleep();
expect(store.isFetching).toBe(false);
});
processAction is synchronous and unaware of promises and this results in a dangling promise. Dangling promises should never reject because this results in unhandled rejection, which is a kind of exception. This may cause an application to crash depending on the environment. Even if exceptions are handled globally, this shouldn't be an reason to not handle errors where they are expected.
A correct way to do this is to suppress a rejection explicitly either in fetchTheThing where it occurs:
private async fetchTheThing() {
try {
...
} catch {} finally {
this.isFetching = false;
}
}
Or in this case, it's more like processAction that results in dangling promise:
this.fetchTheThing().catch(() => {});
Otherwise unhandled rejection event is dispatched.
Without that, it could be tested by listening for the event:
...
let onRej = jest.fn();
process.once('unhandledRejection', onRej);
rej();
await Promise.sleep();
expect(onRej).toBeCalled();
expect(store.isFetching).toBe(false);
This won't work as intended if there's already another unhandledRejection listener, which can be expected in a good Jest setup. If this is the case, the only workaround that won't affect other tests is to reset them before the test and re-add afterwards:
let listeners;
beforeEach(() => {
listeners = process.rawListeners('unhandledRejection');
process.removeAllListeners('unhandledRejection');
});
afterEach(() => {
(typeof listeners === 'function' ? [listeners] : listeners).forEach(listener => {
process.on('unhandledRejection', listener);
});
})
This isn't recommended and should be used at own risk because this indicates a deeper problem with error handling that is not generally acceptable in properly designed JavaScript application.
My function calls a private async function in a fire-and-forget manner, and does not add any error handling.
Don't do that.
An external library handles any and all uncaught promise rejections. In production, I want the shell to handle it, so I do not want to handle it in the function itself.
Don't rely on this external library.
You should have your own global error handling function that you use in your function.
In production, have that error handling function simply rethrow the exception so that it gets picked up by the environment, or better, do call the shell error handling function directly if possible.
In the tests, you can mock out your own global handler, and assert that it is called with the expected arguments.

How to trigger React error boundaries when Promises are rejected? [duplicate]

I want to build a React-component which loads the data asynchronously on componentDidMount.
This is what the function currently looks like (written in TypeScript):
async componentDidMount(): Promise<void> {
try {
const { props: { teamId }, state: { } } = this;
const awaitableData = await UrlHelper.getDataAsync("some-fancy-url");
// ... do something with awaitableData
} catch(e) {
console.log("Some error occured");
throw e;
}
}
The render-function returns the markup wrapped in a ErrorBoundary component, which has componentDidCatch implemented.
However, this is never called/triggered when the awaited call is rejected and I end up in the catch-block.
What am I missing here?
async function is syntactic sugar for a regular function that returns a promise. An error in async function results in rejected promise. Even if rejected promise isn't handled anywhere and results in Uncaught (in promise) error, it is not caught by error boundaries.
As the reference states,
Error boundaries do not catch errors for: <...> Asynchronous code (e.g. setTimeout or requestAnimationFrame callbacks)
A solution is to change component state on error and handle it on next render. render is the place where error can be re-thrown synchronously.
An example:
state = { error: null };
async componentDidMount() {
try {
await new Promise(resolve => setTimeout(resolve, 2000));
throw new Error('Foo error');
} catch (error) {
this.setState({ error });
}
}
render() {
if (this.state.error) {
throw this.state.error;
}
return (
<p>Foo</p>
);
}
Using the above or below example with a functional component
provided a solution, something like this, for me:
const SomComponent: JSX.Element = () => {
const [state, setState] = useState({error: null, info: null})
... // something here setState the error
if (state.error) {
throw new Error('example') // Example throws to the error boundary
}
return <div> Some Component </div>
}
lets take a look to the Documentation
basically it says:
Error boundaries are React components that catch JavaScript errors
anywhere in their child component tree, log those errors, and display
a fallback UI instead of the component tree that crashed. Error
boundaries catch errors during rendering, in lifecycle methods, and in
constructors of the whole tree below them.
so basically when you are trying to use the ASYNC/AWAIT and it fails it will go out for the CATCH side of your function:
catch(e) {
console.log("Some error occured");
throw e;
}
and the error will not be thrown by the componentDidMount. actually if you remove the try catch method, the componentDidMount will take care of the error.

Use ErrorBoundary together with asynchronous lifecycle-functions

I want to build a React-component which loads the data asynchronously on componentDidMount.
This is what the function currently looks like (written in TypeScript):
async componentDidMount(): Promise<void> {
try {
const { props: { teamId }, state: { } } = this;
const awaitableData = await UrlHelper.getDataAsync("some-fancy-url");
// ... do something with awaitableData
} catch(e) {
console.log("Some error occured");
throw e;
}
}
The render-function returns the markup wrapped in a ErrorBoundary component, which has componentDidCatch implemented.
However, this is never called/triggered when the awaited call is rejected and I end up in the catch-block.
What am I missing here?
async function is syntactic sugar for a regular function that returns a promise. An error in async function results in rejected promise. Even if rejected promise isn't handled anywhere and results in Uncaught (in promise) error, it is not caught by error boundaries.
As the reference states,
Error boundaries do not catch errors for: <...> Asynchronous code (e.g. setTimeout or requestAnimationFrame callbacks)
A solution is to change component state on error and handle it on next render. render is the place where error can be re-thrown synchronously.
An example:
state = { error: null };
async componentDidMount() {
try {
await new Promise(resolve => setTimeout(resolve, 2000));
throw new Error('Foo error');
} catch (error) {
this.setState({ error });
}
}
render() {
if (this.state.error) {
throw this.state.error;
}
return (
<p>Foo</p>
);
}
Using the above or below example with a functional component
provided a solution, something like this, for me:
const SomComponent: JSX.Element = () => {
const [state, setState] = useState({error: null, info: null})
... // something here setState the error
if (state.error) {
throw new Error('example') // Example throws to the error boundary
}
return <div> Some Component </div>
}
lets take a look to the Documentation
basically it says:
Error boundaries are React components that catch JavaScript errors
anywhere in their child component tree, log those errors, and display
a fallback UI instead of the component tree that crashed. Error
boundaries catch errors during rendering, in lifecycle methods, and in
constructors of the whole tree below them.
so basically when you are trying to use the ASYNC/AWAIT and it fails it will go out for the CATCH side of your function:
catch(e) {
console.log("Some error occured");
throw e;
}
and the error will not be thrown by the componentDidMount. actually if you remove the try catch method, the componentDidMount will take care of the error.

Jasmine .and.throwError() is not caught by .catch in original code

I'm writing a test for a function, and have to trigger the .catch part of that function, but Jasmine's spiesfor some reason can't do that.
Method to be tested:
foo(){
doStuff()
.catch((error) => {
//stuff I still have to test
bar()
})
}
doStuff() returns a Promise (hence the .catch-Setup), but for this test it's supposed to throw an error.
Here is my test:
it('tests the error handling of foo',(done) =>{
spyOn(object,'foo').and.throwError('Test Error');
object.foo();
expect(object.bar).toHaveBeenCalled();
done();
});
Is the way I'm approaching this wrong? Is that an error with Jasmine? (Google didn't find anything)
[I stick to the (done) setup because almost all other tests are async and I want to keep the style]
[I cannot change the code to be tested]
I think I had a problem similar to yours. Here's how I solved it
import { throwError } from 'rxjs';
it(`...`, fakeAsync(() => {
spy = spyOn(authService, 'signIn').and.returnValue(throwError(loginError));
/* do things */
expectSnackbar('error', loginError);
expect(authService.ensureLogin).toHaveBeenCalled();
}));
Here's how the call to the signIn method looks like:
return this.authService
.signIn(payload.email, payload.password)
.map((userId: string) => {
// Whatever
})
.catch(error => {
// Do something with the error
});
And how the thrown error looks like inside signIn
public signIn(email: string, password: string): Observable<string> {
return this.jsonServerService.get('login').pipe(
map(users => {
if (/* valid */) {
return user.userId;
} else {
throw new Error('Error');
}
}),
);
}
If you call and.throwError(...); you throw the error within the test method.
You can try to return a Promise rejection instead:
spyOn(object, 'foo').and.rejectWith(new Error('Test Error'));
and.throwError does not exist when I try it. Maybe I'm using an old version of jasmine.
I got it to work by returning a promise rejection:
and.returnValue(Promise.reject({response: {status: 401}}))

Categories

Resources