9

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.

Julian
  • 8,808
  • 8
  • 51
  • 90
  • "catch | reject" in https://developers.google.com/web/fundamentals/getting-started/primers/promises for your review – Robert Rowntree Aug 03 '17 at 17:10
  • My rule of thumb is that code can/should ever be ran in a synchronous context. For example, I'd have a seperate function for input validation (which is usually sync) so I make it throw. This way I can use it/test it in a synchronous way. – Russley Shaw Aug 03 '17 at 17:14
  • Related article: http://2ality.com/2016/03/promise-rejections-vs-exceptions.html – str Aug 03 '17 at 19:12

3 Answers3

6

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.

peteb
  • 18,552
  • 9
  • 50
  • 62
6

I disagree with whomever told you

never throw an error inside these kinds of promise-based functions

In my opinion, you should throw a TypeError() synchronously when 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 seemingly 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 appears to be 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

Any 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 an exogenous 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.

Community
  • 1
  • 1
Patrick Roberts
  • 49,224
  • 10
  • 102
  • 153
  • The question is not about the difference between Operational and Programmer errors, or how to use them, the question is about what to use in promises: throw error or reject the promise. – alexmac Aug 03 '17 at 17:41
  • 3
    @alexmac this is _absolutely_ about operational vs programmer errors. The question is _Should I throw an error or return a rejected promise inside an async function?_ and the advice given was to _always_ reject. The answer is to _throw for programmer errors, reject for operational errors_, and the feedback is that the advise given was not sound. – Patrick Roberts Aug 03 '17 at 17:48
  • @alexmac see my update, I've added another relevant example of how Node.js itself differentiates between types of errors. – Patrick Roberts Aug 03 '17 at 18:11
  • [I agree with your stance](https://stackoverflow.com/a/21891544/1048572), however we don't know whether the input is bad because of a programmer error or it's an expected operational error. And the reality is that [most exceptions get caught](https://stackoverflow.com/a/21617531/1048572) in some `then` callback anyway, so it doesn't make a big difference - apps no longer crash from programmer errors except you explicitly distinguish operational errors in your `catch` clauses. So better return a rejected promise to make everyone happy, and force them to use proper error handling. – Bergi Aug 03 '17 at 18:16
  • @Bergi That's why I left the "you figure out what type of error it is" in the conclusion, and as for the second point, it's unfortunate, and that's why you use non-generic `Error()`'s so that a simple `instanceof` check will tell you whether to proceed or crash. – Patrick Roberts Aug 03 '17 at 18:19
  • @Bergi Would it be appropriate to delete this answer and repost it in the duplicate you linked? – Patrick Roberts Aug 03 '17 at 18:21
  • @PatrickRoberts Rather not, it would need considerable revision as the other question is not about node or aws. I'd recommend you just post a comment there with a link to here. – Bergi Aug 03 '17 at 18:36
3

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).

robertklep
  • 198,204
  • 35
  • 394
  • 381
  • Is this how you address programmer errors as well? This is a terrible approach for API calls, as it causes invalid input to be processed as if it's valid input, simply treating it like any operational error. – Patrick Roberts Aug 03 '17 at 18:14
  • @PatrickRoberts I have no idea where `input` originates from, and that distinction can't always be made in other code either. Even if it would throw an error, your argument would be moot in case of `async` (as in the keyword) functions. – robertklep Aug 03 '17 at 18:26
  • I'm referring to your first example – Patrick Roberts Aug 03 '17 at 18:27
  • @PatrickRoberts you mean returning `Promise.reject()`? Again, I have no idea where `input` is coming from. – robertklep Aug 03 '17 at 18:31
  • The point is, a type assertion is a programmer error, not an operational error. It doesn't matter where it came from, if it's the wrong type of input, you crash. – Patrick Roberts Aug 03 '17 at 18:32
  • @PatrickRoberts I can't agree with that. If the input is ultimately passed in by a third party, and it's faulty, you don't crash. It might still be a programmer error, but it's a third party programmer. – robertklep Aug 03 '17 at 18:36
  • So, given the choice, you'd prefer to make debugging usage of your API _more_ difficult? All `reject()` does for a programmer error is to obscure where it actually happens, and there's literally no downside in synchronously throwing when it's a programmer error. That anti-pattern you have in the first block? That's what an oblivious developer would do to suppress programmer errors. In good code, there'd be no try-catch wrapping the chain, since we _want_ programmer errors to crash as quickly as possible making API calls easier to debug, and deal with it by fixing the bug rather than hiding it. – Patrick Roberts Aug 03 '17 at 18:56