Ignore a rejected fire-and-forget promise in jest - javascript

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.

Related

How to avoid ` PromiseRejectionHandledWarning: Promise rejection was handled asynchronously`?

I'm getting PromiseRejectionHandledWarning: Promise rejection was handled asynchronously warning for my code, it also fails Jest tests. I've read that's Promise rejection should be handled at the same place they're defined and seem to understand the logic and the reasons. But it looks like something is wrong with my understanding as what I expect to be synchronous handling still cause warning.
My typescript code is below.
Promise, that is rejected is sendNotification(subscription, JSON.stringify(message)). From my understanding it's handled right away using .catch call, but probably I'm missing something. Can anyone, please, point me to my mistake?
private notify(tokens: string[], message: IIterableObject): Promise<any> {
const promises = [];
tokens.forEach(token => {
const subscription = JSON.parse(token);
this.logger.log('Sending notification to subscription', {subscription, message})
const result = this.WebPushClient
.sendNotification(subscription, JSON.stringify(message))
.catch(e => {
this.logger.log('Send notification failed, revoking token', {
subscription,
message,
token,
e
})
return this.revokeToken(token).catch(error => {
this.logger.error('Failed to revoke token', {
token,
error,
})
return Promise.resolve();
});
});
promises.push(result);
});
return Promise.all(promises);
}
I found the issue.
In Jest you can't just mock return value with rejected promise. You need to wrap it in special workaround function:
const safeReject = p => {
p.catch(ignore=>ignore);
return p;
};
And then wrap the Promise before return it
const sendNotification = jest.fn();
sendNotification.mockReturnValue(safeReject(Promise.reject(Error('test error'))));
You can also use mockImplementation (I'm using jest 28).
jest.fn().mockImplementation(() => Promise.reject(new Error('test')))

Testing an asynchronous throw error in a Promise catch with Jest

I have the following code that I'd like to test.
const Component: React.FC = () => {
const handleSubmit = (action) => {
doSomethingAsynchronous()
.then(() => /* something on success */)
.catch((err) => {
// Display the error message
action();
// Rethrow the exception so it can be handled up the chain
throw err;
})
}
return <Form onSubmit={handleSubmit} />;
}
This code performs a simple asynchronous action that fails or resolves. On a failure, the component is re-rendered to show an error message, and the error is rethrown to log to the console/our logging system and for parent components to deal with.
The problem comes when I am attempting to test the error handling behaviour to ensure that the error messages are being set. Simple testing such as:
describe('Component', () => {
it('handles an error', async () => {
// Setup
const mockAction = jest.fn();
const render = shallowRender(<Component />);
submissionHandler = render.find(Component).invoke('onSubmit');
// Act
submissionHandler(mockAction);
await () => new Promise(setImmediate); // To wait for promise execution
// Assert
expect(mockAction).toHaveBeenCalled();
})
})
Results in Jest failing the test as an error has been thrown in the test by the component, inside the catch block (as expected). However, my attempts to suppress this also result in the same error being thrown and failing the test.
try {
// Act
submissionHandler(mockAction);
await () => new Promise(setImmediate); // To wait for promise execution
} catch (e) {}
I also tried using expects().toThrow(), but this instead returns the jest error Received function did not throw. I assume this is because due to the promise the execution is no longer in the same function scope, so isn't being recognised by Jest as originating from that function?
await expect(async () => {
submissionHandler(mockAction);
await () => new Promise(setImmediate);
}).toThrow();
Does anyone know the best way to test this? I'm aware I can cheat by making onSubmit return my promise here and catching the exception there, but I'd avoid doing that to stop my function returning for testing purposes.
You need to unpack your errors from your promise with .rejects
try this:
import { spyOn } from 'jest-mock';
...
it("should error", async() => {
spyOn(console, 'error'); #removes error from output
await expect( yourAsyncMethod() ).rejects.toThrow() # .rejects unpacks errors from promises
}

How to test status of a promise inside Promise.finally() without awaiting it in production code

I am using Promise.prototype.finally() (or try-catch-finally in an async function) in my production code to execute some follow-up code without changing resolution/rejection status of the current promise.
However, in my Jest tests, I would like to detect that the Promise inside finally block wasn't rejected.
edit: But I don't want to actually await the Promise in my "production" code (there I care only about errors re-thrown from catch, but not about errors from finally).
How can I test for that? Or at least how to mock the Promise.prototype to reject the current promise on exceptions from finally?
E.g. if I would be testing redux action creators, the tests pass even though there is a message about an unhandled Promise rejection:
https://codesandbox.io/s/reverent-dijkstra-nbcno?file=/src/index.test.js
test("finally", async () => {
const actions = await dispatchMock(add("forgottenParent", { a: 1 }));
const newState = actions.reduce(reducer, undefined);
expect(newState).toEqual({});
});
const dispatchMock = async thunk => {...};
// ----- simplified "production" code -----
const reducer = (state = {}, action) => state;
const add = parentId => async dispatch => {
dispatch("add start");
try {
await someFetch("someData");
dispatch("add success");
} catch (e) {
dispatch("add failed");
throw e;
} finally {
dispatch(get(parentId)); // tests pass if the promise here is rejected
}
};
const get = id => async dispatch => {
dispatch("get start");
try {
await someFetch(id);
dispatch("get success");
} catch (e) {
dispatch("get failed");
throw e;
}
};
const someFetch = async id => {
if (id === "forgottenParent") {
throw new Error("imagine I forgot to mock this request");
}
Promise.resolve(id);
};
dispatch(get(parentId)); // tests pass if an exception is thrown here
There is no exception throw in that line. get(parentId) might return a rejected promise (or a pending promise that will get rejected later), but that's not an exception and won't affect control flow.
You might be looking for
const add = parentId => async dispatch => {
dispatch("add start");
try {
await someFetch("someData");
dispatch("add success");
} catch (e) {
dispatch("add failed");
throw e;
} finally {
await dispatch(get(parentId));
// ^^^^^
}
};
Notice that throwing exceptions from a finally block is not exactly a best practice though.
edit: more general solutions available on https://stackoverflow.com/a/58634792/1176601
It is possible to store the Promise in a variable accessible in some helper function that is used only for the tests, e.g.:
export const _getPromiseFromFinallyInTests = () => _promiseFromFinally
let _promiseFromFinally
const add = parentId => async dispatch => {
...
} finally {
// not awaited here because I don't want to change the current Promise
_promiseFromFinally = dispatch(get(parentId));
}
};
and update the test to await the test-only Promise:
test("finally", async () => {
...
// but I want to fail the test if the Promise from finally is rejected
await _getPromiseFromFinallyInTests()
});

Alternative for `return await …` for the cases when waiting for some result which must be returned

I know that ESLint/TSLint rulest cannot be "right" for 100% of situations. However I need to decide which rules I really don't need.
In ElectonJS, it's not recommended to use Node.js modules in Renderer Process. Instead, Renderer process should send request to Main Process and listen for the response. Below TypeScript class takes care about this routine. (I hope my variable names make code does not need comments, but those are hard to understand, please let me to know in comments)
import { ipcRenderer as IpcRenderer } from "electron";
import InterProcessDataTransferProtocol from "#ProjectInitializer:Root/InterProcessDataTransferProtocol";
import CheckingPathForWriteAccess = InterProcessDataTransferProtocol.CheckingPathForWriteAccess;
import MainProcessEvents = InterProcessDataTransferProtocol.MainProcessEvents;
import Timeout = NodeJS.Timeout;
export default abstract class InterProcessFacilitator {
private static readonly IS_PATH_WRITABLE__RESPONSE_WAITING_PERIOD__MILLISECONDS: number = 10000;
public static async requestCheckingPathForAccessToWrite(targetPath: string): Promise<boolean> {
IpcRenderer.send(
InterProcessDataTransferProtocol.RendererProcessEvents.checkingPathForWriteAccessRequestSent,
{ targetPath }
);
const responseWaitingTimeout: Timeout = setTimeout(
() => { throw new Error("No response from Main Process"); },
InterProcessFacilitator.IS_PATH_WRITABLE__RESPONSE_WAITING_PERIOD__MILLISECONDS
);
return await new Promise<boolean>((resolve: (isWritable: boolean) => void): void => {
IpcRenderer.on(
MainProcessEvents.checkingPathForWriteAccessDone,
(_event: Electron.Event, responsePayload: CheckingPathForWriteAccess.ResponsePayload) =>
{
clearTimeout(responseWaitingTimeout);
if (responsePayload.targetPath === targetPath) {
resolve(responsePayload.isWritable);
}
});
});
}
}
Currently, method requestCheckingPathForAccessToWrite violates no-return-await, the ESLint rules. However it could be used as:
async function checkTheDefaultPathForWrightPermission(): Promise<void> {
try {
const pickedPathIsWritable: boolean = await InterProcessFacilitator
.requestCheckingPathForAccessToWrite(DEFAULT_PATH);
pickedPathIsWritable ?
this.relatedStoreModule.reportAboutUnableToWriteToDirectoryErrorResolution() :
this.relatedStoreModule.reportAboutUnableToWriteToDirectoryErrorOccurrence();
} catch (error) {
console.error(`unable to check wright permission for path ${DEFAULT_PATH}`);
console.error(error);
}
}
From ESLint documentation:
Inside an async function, return await is seldom useful. Since the
return value of an async function is always wrapped in
Promise.resolve, return await doesn’t actually do anything except add
extra time before the overarching Promise resolves or rejects. The
only valid exception is if return await is used in a try/catch
statement to catch errors from another Promise-based function.
Can you criticize either my solution or this ESLint rule, and in first case, suggest the refactoring?

Proper Promise handling success and error callbacks

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.

Categories

Resources