This question already has an answer here:
-
Should an async API ever throw synchronously?
3 answers
I'm working with the promises provided by the AWS JS SDK. The gist of what I'm doing when I create an async function that wraps the AWS SDK looks like this:
module.exports.myCustomFunction = input => {
if (badInput) {
throw new Error('failed') // <-- This is they key line
}
return sns.createTopic(someParams).promise()
}
// The caller looks like this:
myModule.myCustomFunction(someInput).then(result => {
// carry on
})
.catch(err => {
// do something with the error
})
I was approached by someone who said that I should never throw an error inside these kinds of promise-based functions. They suggested returning Promise.reject('failed')
instead. To be honest I'm not that well versed in promises yet so their explanation kind of went over my head.
They are correct.
The call to myCustomFunction
assumes that a promise is returned at all times (.then
and .catch
deal with resolved and rejected promises, respectively). When you throw an error, the function doesn't return a promise.
You could use this to catch the error:
try {
myModule.myCustomFunction(someInput).then(result => {
// carry on
})
.catch(err => {
// do something with the error
})
} catch(err) {
...
}
But as you can see, this results in two error handlers: try/catch
for the synchronously thrown error, and .catch
for any rejected promises that sns.createTopic(someParams)
may return.
That's why it's better to use Promise.reject()
:
module.exports.myCustomFunction = input => {
if (badInput) {
return Promise.reject('failed');
}
return sns.createTopic(someParams).promise()
}
Then, the .catch
will catch both types of errors/rejections.
NB: for newer versions of Node.js (v7.6 and up, I believe), the following will also work:
module.exports.myCustomFunction = async input => {
if (badInput) {
throw new Error('failed');
}
return sns.createTopic(someParams).promise()
}
The key here is the async
keyword. By using this keyword, the function results are wrapped by a promise automatically (similar to what @peteb's answer is showing).
A throw
within a Promise/Promise chain will automatically cause that Promise/Promise Chain to be rejected.
const myFunc = input => {
return new Promise((resolve, reject) => {
if (badInput) {
throw new Error('failed')
}
return resolve(sns.createTopic(input).promise())
})
}
return myFunc('some input')
.then(result => {
// handle result
})
.catch(err => { console.log(err) }) // will log 'failed'
However, your myCustomFunction
isn't wrapped in a Promise, it is using throw
before the Promise is returned by sns.createTopic().promise()
. To create and return a Promise already in a rejected state you would use Promise.reject(new Error('failed'))
instead of throw
.
I disagree with whoever told you
never throw an error inside these kinds of promise-based functions
In my opinion, you should throw a TypeError()
synchronously, as it indicates a programmer error rather than an operational error.
To quote Joyent | Error Handling:
Operational errors represent run-time problems experienced by correctly-written programs. These are not bugs in the program. In fact, these are usually problems with something else [...]
Programmer errors are bugs in the program. These are things that can always be avoided by changing the code. They can never be handled properly (since by definition the code in question is broken).
Your colleague fails to differentiate between these types of errors, and the code you have written is almost as it should be, with the exception of using a generic Error()
instead of the semantically correct TypeError()
.
Why should you care about the difference?
You started out by saying that you're writing a wrapper for the AWS SDK. So, from the point of view of developers using your library, do you think they'd prefer to debug a program that throws immediately where they're doing something wrong, or would they prefer to debug a program that fails silently, attempting to resolve their misuse of your API without informing them of incorrect code?
If you think the first option sounds easier to deal with, you'd be absolutely right. Your program should always be as transparent as possible when telling a programmer what they've done wrong. Attempting to resolve misuse is what results in buggy APIs with undefined, undocumented, and just plain bizarre behavior.
What were they trying to recommend I do instead?
To give an extremely basic example (and possibly unfair comparison, since I don't have any context as to what constitutes badInput
), your colleague is informing you that you should do this:
try {
if (badInput) {
throw new Error('failed')
}
...
} catch (error) {
// expected error, proceed as normal
// ...except not really, you have a bug
}
instead of this:
process.on('uncaughtException', function (error) {
// deal with programmer errors here and exit gracefully
})
if (badInput) {
throw new Error('failed')
}
try {
...
} catch (error) {
// deal with operational errors here and continue as normal
}
Some real-world examples in the Node.js runtime environment that differentiate these errors, even in asynchronous functions can be found in the chart here:
Example func | Kind of func | Example error | Kind of error | How to | Caller uses
| | | | deliver |
==========================================================================================
fs.stat | asynchronous | file not found | operational | callback | handle callback
| | | | | error
-------------+--------------+----------------+---------------+----------+-----------------
fs.stat | asynchronous | null for | programmer | throw | none (crash)
| | filename | | |
Conclusion
I'll leave it to you to decide whether your particular issue is due to a programmer error or an operational error, but in general, the advice that was given to you is not sound advice, and encourages buggy programs that attempt to proceed as if there was nothing wrong.
TL;DR
An function that is expected to return a Promise
under operational conditions should throw
synchronously when the error is due to a bug, and should reject
asynchronously when the error occurs within a correctly-written program.
This reflects the official recommendation of Joyent:
The best way to recover from programmer errors is to crash immediately.