I went through other similar posts and they all mention that I need to specify catch block with my promise instead of second param in then(onSuccess, onRejected).
However I realize if I change myPromise.then(onSuccess, onRejected) to myPromise.then(onSuccess).catch(onRejected) then catch block is never executed and error still shows up in chrome dev console.
// Verifiers
const verifyOnFulfilled = (result) => {
expect(result).toEqual(expectedOnFulfilledResponse);
};
const verifyOnRejected = (result) => {
expect(result).toEqual(expectedOnRejectedResponse);
};
// Mocking a server call and this catch statement does nothing.
// Added just to test if this fixes the prob
mockPromise.catch((e) => {
return e;
});
// Mock request bind. Ignore this
requestSpy.and.returnValue(mockPromise);
// Call the api
const promiseA = myService.deleteItem(id); // It will create a new mock promise
promiseA.then(verifyOnFulfilled, verifyOnRejected);
// promiseA.then(verifyOnFulfilled).catch(verifyOnRejected); // If I use this then catch function is never executed.
// Run mock promises
MockPromises.executeForPromise(promiseA);
API call block:
let deleteRequestPromise = ... // some internal generic way to create promise
return deleteRequestPromise.then(
() => {
return commentId;
},
(error) => {
return Promise.reject(error);
}
);
Related
I have a function that returns a promise, but I want to test that it has a catch defined, and then additionally test that it re-throws the error.
This is a very contrived example but it was the clearest way to show the issue. In my actual code, I am calling a function that is mocked to fail (vs the manually rejecting in this example), and I have additional logging in the catch statement, which explains the re-throwing of the error.
const foo = () => {
return new Promise((resolve, reject) => {
reject(new Error('reject')); // manually rejecting to mimic actual code...
}).catch(error => {
// do some additional logging...
throw error;
});
};
it('should catch and re-throw error', () => {
// Received function did not throw
// and
// Unhandled promise rejection
expect(() => foo()).toThrow();
// Test passes, even when `throw error` is commented out with false positive
expect(foo()).rejects.toThrow();
});
I can successfully check that the logging function is called, but can't figure out how to ensure the error is re-thrown after.
WORKING UPDATE :)
thanks to #skyboyer & #Bergi for getting me to think about the issue a bit differently, and exposing me to some of the finer points of jest
Below is both the updated code to show the logging function, and the updated tests i settled on.
The issues that led to this were
unable to test logging was called due to the error being re-thrown
unable to test the value of the error being re-thrown
Catching the rejected promise allowed me to do both.
I was going to leave in the rejects.toEqual test, but it seems redundant now...
interested in any feedback! and thanks again!
// myModule.js
export const logging = () => {};
export const bar = () => new Promise(resolve => {});
export const foo = () => {
return bar().catch(error => {
logging();
throw error;
});
};
describe('myModule', () => {
let fooReturn;
beforeEach(() => {
jest.clearAllMocks();
jest.spyOn(myModule, 'bar').mockImplementation(() => {
return Promise.reject({ error: 'bar error' });
});
jest.spyOn(myModule, 'logging').mockImplementation(() => {});
fooReturn = myModule.foo();
});
it('should catch and re-throw error', () => {
expect.assertions(1);
fooReturn.catch(result => expect(result).toEqual({ error: 'bar error' }));
// removed since the above test covers that the promise was rejected
// return fooReturn.rejects.toEqual(expect.anything());
});
it('should call the loggin method', async () => {
expect.assertions(1);
// prevents UnhandledPromiseRejectionWarning
fooReturn.catch(() => {});
expect(myModule.logging).toBeCalled();
});
});
You missed return.
https://jestjs.io/docs/asynchronous#resolves--rejects
Be sure to return the assertion—if you omit this return statement, your test will complete before the promise returned from fetchData is resolved and then() has a chance to execute the callback.
Your test should be
it('should catch and re-throw error', () => {
return expect(foo()).rejects.toEqual(expect.anything());
});
As u/Bergi noticed with async/await it may look more laconic:
it('should catch and re-throw error', async () => {
await expect(foo()).rejects.toEqual(expect.anything());
});
but if we miss to add await before our expect we will have exact the same issue as in version 1 without return. So beware.
I've been trying to find a wrapper that does fetch with retries, timeouts, aborts, etc. I came across https://pastebin.com/54Ct4xEh a little bit ago, and after fixing a couple typos (missing options. and =>), it works, except... well, maybe it works, but I don't know how to use it. How do I abort a fetch with this particular wrapper? I have a fiddle, https://jsfiddle.net/1fdwb2o6/2/. With this code, how can I, say, click a button and have it abort this fetch loop? For my use case, I' using boopstrap, and I have a modal that, when shown, attempts to load dynamic content. If the user clicks Cancel while it's loading, I want the fetch process to stop. From what I can tell, I should be able to do it with the code below... but I'm not sure how to perform the abort. Perhaps this isn't possible, as structured, with a Promise... but I don't know enough (anything) about promises to know better, one way or the other.
const fetchWithRetry = (userOptions) => {
let abort = false;
const options = {
url: '',
options: {},
cancel: {},
retries: 5,
retryDelay: 1000,
...userOptions
};
// Add an abort to the cancel object.
options.cancel.abort = () => {
abort = true;
};
// Abort or proceed?
return abort ? Promise.reject('aborted') : fetch(options.url).then(response => {
// Reject because of abort
return abort ? Promise.reject('aborted')
// Response is good
: response.ok ? Promise.resolve(response.text())
// Retries exceeded
: !options.retries ? Promise.reject('retries exceeded')
// Retry with one less retry
: new Promise((resolve, reject) => {
setTimeout(() => {
// We use the returned promise's resolve and reject as
// callback so that the nested call propagates backwards.
fetchWithRetry({
...options,
retries: options.retries - 1
}).then(resolve, reject);
}, options.retryDelay);
});
});
}
var xxx;
console.clear();
xxx = fetchWithRetry({
url: "some_file_that_doesnt_exist.php"
})
.then((response) => {
alert(response);
}).catch(function(err) {
// Error: response error, request timeout or runtime error
alert("Error! Cannot load folder list! Please try again!");
});
setTimeout(function() {
// somehow, abort the fetch...
// xxx.abort(); <-- no worky...
}, 1234);
As I said in my comments, the code you have in your question does not provide a cancel() function that the caller can use. It has a cancel() function internally, but that's not something the caller can use. As written that function just returns a promise so the caller has nothing they can call to cancel the retries.
So, I decided to write my own version of fetchWithRetry() that would work for your use case. This has a number of capabilities that the one in your question does not:
It returns both the promise and a cancel function so the caller can cancel the retries.
It allows you to pass the init options for fetch() so you can pass any of the various arguments that fetch() supports and are often needed such as withCredentials.
It has an option to check the response.ok boolean so it will detect and retry more things that you would if you required the promise to be rejected before a retry (note: fetch() doesn't reject on a 404, for example).
If There was a fetch() rejection and it was either cancelled or it ran out of retries, then it will use the newest Error class feature where it will set the cause to the actual fetch() error so the caller can see what the original error was.
Note that this version of fetchWithRetry() returns an object containing both a promise and a cancel function. The caller uses the promise the same way they would any promise from fetch() and they can use the cancel() function to cancel any further retries.
Here's the code:
const Deferred = function() {
if (!(this instanceof Deferred)) {
return new Deferred();
}
const p = this.promise = new Promise((resolve, reject) => {
this.resolve = resolve;
this.reject = reject;
});
this.then = p.then.bind(p);
this.catch = p.catch.bind(p);
if (p.finally) {
this.finally = p.finally.bind(p);
}
}
function fetchWithRetry(url, userOptions = {}, init = {}) {
const options = {
// default options values, can be overridden by userOptions
retries: 3,
retryDelay: 1000,
checkResponseOk: true,
...userOptions
};
let cancelled = false;
let timerDeferred;
let timer;
function run() {
return fetch(url, init).then(response => {
// force retry on non 2xx responses too
if (options.checkResponseOk && !response.ok) {
throw new Error(`fetch failed with status ${response.status}`);
}
return response;
}).catch(err => {
// got error, set up retry
console.log(err);
if (cancelled) {
throw new Error("fetch cancelled", { cause: err });
}
--options.retries;
if (options.retries < 0) {
throw new Error("fetch max retries exceeded", { cause: err });
}
// create new Deferred object for use with our timer
// so it can be resolved by the timer or rejected
// by the cancel callback
timerDeferred = new Deferred();
timer = setTimeout(() => {
timerDeferred.resolve();
timer = null;
}, options.retryDelay);
return timerDeferred.then(() => {
if (cancelled) {
throw new Error("fetch cancelled", { cause: err });
}
return run();
});
});
}
return {
promise: run(),
cancel: () => {
cancelled = true;
// if currently in a timer waiting, reject immediately
if (timer) {
clearTimeout(timer);
timer = null;
}
if (timerDeferred) {
timerDeferred.reject(new Error("fetch cancelled"));
}
}
}
};
Sample usage:
const result = fetchWithRetry(someUrl);
result.promise.then(resp => {
return resp.text().then(data => {
// got final result here
console.log(data.slice(0, 100));
});
}).catch(err => {
console.log(err);
});
// simulate user cancel after 1.5 seconds
setTimeout(() => {
result.cancel();
}, 1500);
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
}
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()
});
I've got some basic Javascript code that calls a stock API with symbols where the symbols are provided from a simple HTTP call like this:
GET http://localhost:4000/batch_stock_prices/?stocks=12312.
I believe I am misunderstanding the syntax for how to catch an exception from a promise..
An exception gets thrown that 12312 is an invalid symbol which I expect, on the terminal running the node server I see the exception but I'm not able to pass it back in the HTTP response. The error that's passed back in the response is 'undefined'. How can I catch the exception? Do I need a try catch somewhere?
const fetch = require('node-fetch')
const { IEXCloudClient } = require("node-iex-cloud");
const { type } = require('tap');
const iex = new IEXCloudClient(fetch, {
sandbox: true,
publishable: "pk_2f78524e5........23c327e24b5",
version: "stable"
});
'use strict'
async function getCurrentPriceOfBatchStocks(_stock) {
stocks_to_submit = _stock['stocks'];
console.log(stocks_to_submit)
response = await iex
.batchSymbols(stocks_to_submit)
.price()
.catch(function (error) { // <-- doesn't seem to get called
console.log("Exception: " + error);
throw error;
})
return new Promise((resolve, reject) => {
if (response) {
resolve(response)
} else {
reject(response); // <-- response is undefined. why?
}
});
}
const batchStocksSchema = {
querystring: {
type: 'object',
properties: {
stocks: {
type: 'string'
}
},
required: ['stocks']
}
}
module.exports = async function (fastify, opts) {
fastify.get('/batch_stock_prices/', {
schema: batchStocksSchema
}, async function (request, reply) {
try {
var response = await getCurrentPriceOfBatchStocks(request.query)
// console.log(response)
return reply
.code(200)
.send(response);
} catch (e) {
console.log(e)
return reply
.code(400)
.send('Bad Request, exception: ' + e) // outputs: ...exception: undefined
}
})
}
It's hard to say for sure what's wrong without running the code, but there are several issues with the use of async, await, and promises in the code, and with creating implicit globals. (Also various missing ;.) If we sort those out, it may be that whatever error is occurring will stop being obscured. See *** comments:
"use strict"; // *** This has to be at the very beginning of the compilation
// unit, it can't be later in the code as it is in the question
const fetch = require('node-fetch')
const { IEXCloudClient } = require("node-iex-cloud");
const { type } = require('tap');
const iex = new IEXCloudClient(fetch, {
sandbox: true,
publishable: "pk_2f78524e5........23c327e24b5",
version: "stable"
});
async function getCurrentPriceOfBatchStocks(_stock) {
// *** Declare `stocks_to_submit`
const stocks_to_submit = _stock['stocks'];
// *** Declare `response`
const response = await iex.batchSymbols(stocks_to_submit).price();
// *** Don't catch the error, let it propagate; the caller should
// know whether the call succeeded or failed
// *** Don't use `new Promise`, there's no purpose to it
return response;
}
const batchStocksSchema = {
querystring: {
type: 'object',
properties: {
stocks: {
type: 'string'
}
},
required: ['stocks']
}
};
// *** This function never uses `await`, so don't make it `async`
module.exports = function (fastify, opts) {
fastify.get('/batch_stock_prices/', {
schema: batchStocksSchema
}, function (request, reply) { // *** Typically old-style callback APIs don't do
// anything with the promise an `async` function
// returns, so don't pass `async` functions into them
getCurrentPriceOfBatchStocks(request.query)
.then(response => {
// *** No `return` here, we aren't resolving the promise from `then` with the result
// of `send`
reply
.code(200)
.send(response);
})
.catch(e => {
console.log(e);
// *** No `return` here, we aren't resolving the promise from `catch` with the
// result of `send`
reply
.code(400)
.send('Bad Request, exception: ' + e);
});
});
};
For why the catch is not called in this part:
response = await iex
.batchSymbols(stocks_to_submit)
.price()
.catch(function (error) { // <-- doesn't seem to get called
console.log("Exception: " + error);
throw error;
})
and why response is undefined:
return new Promise((resolve, reject) => {
if (response) {
resolve(response)
} else {
reject(response); // <-- response is undefined. why?
} });
This is the cause:
The promise returned by price() call had resolves an undefined value (instead of rejecting with an error). Your "await" wait for this undefined value and assigned it to "response" variable.
The price() when having problem have already handled the error and then print the details in the console:
Error: <html>
<head><title>400 Bad Request</title></head>
<body bgcolor="white">
<center><h1>400 Bad Request</h1></center>
<hr><center>nginx</center>
</body>
</html>
at IEXRequest.<anonymous> (/home/runner/ArtisticAridSite/node_modules/node-iex-cloud/lib/request.js:128:35)
at step (/home/runner/ArtisticAridSite/node_modules/node-iex-cloud/lib/request.js:32:23)
at Object.next (/home/runner/ArtisticAridSite/node_modules/node-iex-cloud/lib/request.js:13:53)
at fulfilled (/home/runner/ArtisticAridSite/node_modules/node-iex-cloud/lib/request.js:4:58)
It wasn't really passing the error back in the chain to your code.
So on your question of "How can I catch the exception?". Unfortunately you probably has no way to receive the exception details (unless you can control the error handling behaviour of iex). You may consider to check whether the result is undefined and handle accordingly.