How can I reject a promise that returned by an async
/await
function?
e.g. Originally:
I have a suggestion to properly handle rejects in a novel approach, without having multiple try-catch blocks.
import to from './to';
async foo(id: string): Promise<A> {
let err, result;
[err, result] = await to(someAsyncPromise()); // notice the to() here
if (err) {
return 400;
}
return 200;
}
Where the to.ts function should be imported from:
export default function to(promise: Promise<any>): Promise<any> {
return promise.then(data => {
return [null, data];
}).catch(err => [err]);
}
Credits go to Dima Grossman in the following link.
I know this is an old question, but I just stumbled across the thread and there seems to be a conflation here between errors and rejection that runs afoul (in many cases, at least) of the oft-repeated advice not to use exception handling to deal with anticipated cases. To illustrate: if an async method is trying to authenticate a user and the authentication fails, that's a rejection (one of two anticipated cases) and not an error (e.g., if the authentication API was unavailable.)
To make sure I wasn't just splitting hairs, I ran a performance test of three different approaches to that, using this code:
const iterations = 100000;
function getSwitch() {
return Math.round(Math.random()) === 1;
}
function doSomething(value) {
return 'something done to ' + value.toString();
}
let processWithThrow = function () {
if (getSwitch()) {
throw new Error('foo');
}
};
let processWithReturn = function () {
if (getSwitch()) {
return new Error('bar');
} else {
return {}
}
};
let processWithCustomObject = function () {
if (getSwitch()) {
return {type: 'rejection', message: 'quux'};
} else {
return {type: 'usable response', value: 'fnord'};
}
};
function testTryCatch(limit) {
for (let i = 0; i < limit; i++) {
try {
processWithThrow();
} catch (e) {
const dummyValue = doSomething(e);
}
}
}
function testReturnError(limit) {
for (let i = 0; i < limit; i++) {
const returnValue = processWithReturn();
if (returnValue instanceof Error) {
const dummyValue = doSomething(returnValue);
}
}
}
function testCustomObject(limit) {
for (let i = 0; i < limit; i++) {
const returnValue = processWithCustomObject();
if (returnValue.type === 'rejection') {
const dummyValue = doSomething(returnValue);
}
}
}
let start, end;
start = new Date();
testTryCatch(iterations);
end = new Date();
const interval_1 = end - start;
start = new Date();
testReturnError(iterations);
end = new Date();
const interval_2 = end - start;
start = new Date();
testCustomObject(iterations);
end = new Date();
const interval_3 = end - start;
console.log(`with try/catch: ${interval_1}ms; with returned Error: ${interval_2}ms; with custom object: ${interval_3}ms`);
Some of the stuff that's in there is included because of my uncertainty regarding the Javascript interpreter (I only like to go down one rabbit hole at a time); for instance, I included the doSomething
function and assigned its return to dummyValue
to ensure that the conditional blocks wouldn't get optimized out.
My results were:
with try/catch: 507ms; with returned Error: 260ms; with custom object: 5ms
I know that there are plenty of cases where it's not worth the trouble to hunt down small optimizations, but in larger-scale systems these things can make a big cumulative difference, and that's a pretty stark comparison.
SO… while I think the accepted answer's approach is sound in cases where you're expecting to have to handle unpredictable errors within an async function, in cases where a rejection simply means "you're going to have to go with Plan B (or C, or D…)" I think my preference would be to reject using a custom response object.
You can create a wrapper function that takes in a promise and returns an array with data if no error and the error if there was an error.
function safePromise(promise) {
return promise.then(data => [ data ]).catch(error => [ null, error ]);
}
Use it like this in ES7 and in an async function:
async function checkItem() {
const [ item, error ] = await safePromise(getItem(id));
if (error) { return null; } // handle error and return
return item; // no error so safe to use item
}
It should probably also be mentioned that you can simply chain a catch()
function after the call of your async operation because under the hood still a promise is returned.
await foo().catch(error => console.log(error));
This way you can avoid the try/catch
syntax if you do not like it.
This is not an answer over @T.J. Crowder's one. Just an comment responding to the comment "And actually, if the exception is going to be converted to a rejection, I'm not sure whether I am actually bothered if it's an Error. My reasons for throwing only Error probably don't apply."
if your code is using async
/await
, then it is still a good practice to reject with an Error
instead of 400
:
try {
await foo('a');
}
catch (e) {
// you would still want `e` to be an `Error` instead of `400`
}
Your best bet is to throw
an Error
wrapping the value, which results in a rejected promise with an Error
wrapping the value:
} catch (error) {
throw new Error(400);
}
You can also just throw
the value, but then there's no stack trace information:
} catch (error) {
throw 400;
}
Alternately, return a rejected promise with an Error
wrapping the value, but it's not idiomatic:
} catch (error) {
return Promise.reject(new Error(400));
}
(Or just return Promise.reject(400);
, but again, then there's no context information.)
In your case, as you're using TypeScript
and foo
's return value is Promise<A>
, you'd use this:
return Promise.reject<A>(400 /*or Error*/ );
In an async
/await
situation, that last is probably a bit of a semantic mis-match, but it does work.
If you throw an Error
, that plays well with anything consuming your foo
's result with await
syntax:
try {
await foo();
} catch (error) {
// Here, `error` would be an `Error` (with stack trace, etc.).
// Whereas if you used `throw 400`, it would just be `400`.
}