w00t
w00t

Reputation: 18281

Prevent "Unhandled promise rejection" error

In my server app I want to return a "forbidden" value when the user has no permissions for the endpoint.

To this end I create a rejected promise for reuse:

export const forbidden = Promise.reject(new Error('FORBIDDEN'))

and then elsewhere in the app:

import {forbidden} from './utils'

...

    resolve: (root, {post}, {db, isCollab}) => {
        if (!isCollab) return forbidden
        return db.posts.set(post)
    },

However, when I start my app I get the warning

(node:71620) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: FORBIDDEN
(node:71620) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

How can I tell Node that this Promise is fine to be unhandled?

Upvotes: 5

Views: 5165

Answers (3)

Gershom Maes
Gershom Maes

Reputation: 8150

I wouldn't recommend using the return statement to provide an Error - this is ignoring the exact intention of throw!

Simply use:

if (!isCollab) throw new Error('FORBIDDEN');

If you don't want a stack trace there's no need to over-engineer - simply do:

if (!isCollab) throw 'FORBIDDEN';

If you need a message property to exist you can simply use:

if (!isCollab) throw { message: 'FORBIDDEN' };

(Note: I recommend against throwing anything other than an instance of Error! You'll regret it later when things break and you need to debug)

Upvotes: 2

Craig  Hicks
Craig Hicks

Reputation: 2528

  • OP's usage is not completely described, but the OP's comment "BTW I didn't want to create a stack trace for every forbidden error because I don't want to leak details about my app. So I prefer to create the rejection only once." leads me to believe that at least part of the OP's motivation is to prevent info leakage from unhandled rejections of forbidden.

  • Returning a rejected (but defused) promise behaves differently in a sync vs. an async function. In the former the promise is returned verbatim. In the latter it is rewrapped in a promised and automatically rethrown (equivalent to throwing from inside the function). Whichever use was intended, it makes the program harder to understand.(Wrapping the Promise to be returned in an object or array would solve that problem).

Difference in behavior between sync and async funcs when returning forbidden :

async function test(){
  try {
    let a = await (async()=>{return forbidden;})();  
  } catch(e){console.log(e.message);} // outputs: 'FORBIDDEN'
  try {
    let a = (()=>{return forbidden;})();
    // error undetected  
  } catch(e){console.log(e.message);} // never reaches here !!!
  console.log("something else"); // outputs: something else
  let a=(()=>{return forbidden;})();  // UHR + '#<Promise>' + no addr
  console.log("something else"); // outputs: something else
  await (async()=>{return forbidden;})();  // UHR + '#<Promise>' + no addr leak}
}
test();
  • Regardless of the the OP's usuage, leakage of program info from unhandled-rejections is a valid concern.

The below factory function makeError would provide a general solution, and it builds on the OP's original inspiration:

const verboten=new Error('verbotten');
const makeError = () => verboten;
async function test2(){
  try {
    await (async()=>{throw makeError();})();  
  } catch(e){console.log(e.message);} // outputs: 'verboten'
  // uncomment the following to trigger UHR (unhandled rejection)
  //await (async()=>{throw makeError();})();  // UHR + 'verboten' + no addr leak
}

Note that makeError returns the constant object verboten, rather than itself. (Yes, that is allowed, although it is rarely used.) So the stack trace is a fixed value, unrelated to the error location in the program, just like the original OP's forbidden.

That's fine for the release version, but a minor change to makeError could be made for a development version, where seeing the stack is useful:

const makeError = Error;

Upvotes: 1

Bergi
Bergi

Reputation: 664444

I create a rejected promise for reuse

Well don't, it might be a lot easier to just create a function for reuse:

export function forbidden() { return Promise.reject(new Error('FORBIDDEN')); }

That will also get you an appropriate stack trace for the error every time you call it.

How can I tell Node that this Promise is fine to be unhandled?

Just handle it by doing nothing:

export const forbidden = Promise.reject(new Error('FORBIDDEN'));
forbidden.catch(err => { /* ignore */ }); // mark error as handled

(and don't forget to include the comment about the purpose of this seemingly no-op statement).

Upvotes: 2

Related Questions