UnhandledPromiseRejectionWarning: Callback was already called (Loopback remote method)

Viewed 8227

I'm getting an error that says

(node:27301) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: Callback was already called.

From what I understand about rejecting promises in await's and per the Mozilla description:

If the Promise is rejected, the await expression throws the rejected value.

I reject the error in the callback that's wrapped around my Promise like so:

Airport.nearbyAirports = async (location, cb) => {
  let airports
  try {
    airports = await new Promise((resolve, reject) => {
      Airport.find({
        // code
      }, (err, results) => {
        if (err)
          reject(err) // Reject here
        else
          resolve(results)
      })
    })
  } catch (err) { // Catch here
    cb(err, null)
    return
  }
  if (!airports.empty)
    cb(null, airports)
  }

My question is

  1. Why does it still consider my promise rejection unhandled? I thought the catch statement should silent this error.
  2. Why does it consider my callback already called? I have a return statement in my catch, so both should never be called.
4 Answers

The problem was actually my framework (LoopbackJS), not my function. Apparently at the time of writing this, using promises are not supported:

https://loopback.io/doc/en/lb3/Using-promises.html#setup

Meaning I can't even use await in my function because the remote method wraps my function somewhere else, so async would always be unhandled. I ended up going back to a Promise-based implementation of the inner code:

Airport.nearbyAirports = (location, cb) => {
const settings = Airport.dataSource.settings
const db = DB(settings)
let airports
NAME_OF_QUERY().then((res) => {
  cb(null, res)
}).catch((err) => {
  cb(err, null)
})

If Airport.find() throws an exception, then execution will jump to your catch block and your Promise will never be resolved or rejected. Perhaps you need to wrap it in its own try/catch:

Airport.nearbyAirports = async (location, cb) => {
  let airports
  try {
    airports = await new Promise((resolve, reject) => {
      try {
        Airport.find({
          // code
        }, (err, results) => {
          if (err)
            reject(err) // Reject here
          else
            resolve(results)
        })
      } catch (err) {
        reject(err) // Reject here too
        cb(err, null)
      }
    })
  } catch (err) { // Catch here
    cb(err, null)
    return
  }
  if (!airports.empty)
    cb(null, airports)
  }

We use Loopback 2.31.0 and it also supports simple return for async functions used for remote methods. If you put a break-point somewhere in your remote method and jump one level above it in the call-stack you will see how it is implemented in loopback itself (shared-method.js):

  // invoke
  try {
    var retval = method.apply(scope, formattedArgs);
    if (retval && typeof retval.then === 'function') {
      return retval.then(
        function(args) {
          if (returns.length === 1) args = [args];
          var result = SharedMethod.toResult(returns, args);
          debug('- %s - promise result %j', sharedMethod.name, result);
          cb(null, result);
        },
        cb // error handler
      );
    }
    return retval;
  } catch (err) {
    debug('error caught during the invocation of %s', this.name);
    return cb(err);
  }
};

What it does here - it calls your function and if it is an async function - it will return a promise (retval.then === 'function' will be true). In this case loopback will handle your result correctly, as a promise. It also do the error check for you, so you no longer try/catch blocks in your code anymore.

So, in your own code you just need to use it like below:

Airport.nearbyAirports = async (location) => {
    let airports = await new Promise((resolve, reject) => {
        Airport.find({
            // code
        }, (err, results) => {
            if (err)
                reject(err) // Reject here
            else
                resolve(results)
        })
    });

    if (!airports.empty)
        return airports;
    }
    else  {
        return {}; // not sure what you would like to return here as it wan not handled in your sample...
    }
}

Note, you do not need to use callback (cb) at all here.

Related