Exception in callback of async function: undefined

I’ve been getting a bunch of errors in this format over the past few months:

Exception in callback of async function: undefined

This obviously makes it very hard to debug as I have no idea where the error is coming from and it’s a large application with different tasks running in the background fairly often.

Any idea why it happens, and how I can get a more descriptive error message? This error seems to happen in a bunch of places across the app.

Having a quick look where this message comes from takes me here:

Which is logged out of the logError function.
If we look up, where that’s called, we see it’s used in Meteor.wrapAsync when a callback was not provided to the wrapped function on the client (line 108).

So I would look for places where you’ve used Meteor.wrapAsync on the client, and called the wrapped function without a callback.
If you are able to replicate this locally, you can set a break point in the meteor.js pacakge file where logErr is called and explore up the call stack to find the offending function

The odd part is that it should only log if err is truthy, but your log says undefined? :man_shrugging:

It’s happening on the server. I also don’t see any usage of wrapAsync in my code directly. May be another function that’s calling it.

What Meteor version?

Okay, taking another look, I found this, which has that extra : on the end and runs on the server:

This time it’s in the definition of Meteor.bindEnvironment. So that’s the next thing to search for.

This one doesn’t check if an error was passed in before logging, which also matches the log you quoted. Just need to work out which function isn’t throwing an error!

If you want to put a breakpoint here and track down which function threw the error, it’s going to be a bit harder. The packaged code does end up in .meteor/local/build/programs/server/packages/meteor.js, so you can probably drop a debugger statement in there and run your app with the --inspect flag.
Alternatively, make your own stack trace by logging a new Error on that line

So I managed to reproduce it by throwing an error inside a callback of HTTP.get callback inside of a Meteor method call (although I feel like this happens for many sorts of errors in our system). Will try dig into deeper now

Meteor version: METEOR@1.8.1

So it’s pretty easy to reproduce with setTimeout. A Meteor method that has a setTimeout that throws will do this. For example:

Meteor.methods({
  myMethod() {
    Meteor.setTimeout(() => {
      throw new Meteor.Error('xyz');
    }, 100);
  }
})

And throw:

Exception in setTimeout callback: undefined

At least I know it’s setTimeout in this exception, although undefined message makes it pretty hard to track down if you don’t know where it’s coming from. The async function: undefined error is even worse.

1 Like

hmmm, I’m think it just doesn’t like it when a method throws after it’s already returned to the client.
On the server, HTTP.get doesn’t need a callback, but if it’s a shared method, that’s not an option.
You could promisify the method and return a promise instead?

I am facing the same issue because I have try/catch blocks where I don’t want the error being thrown to interfere in the main thread. My hope with the code below is that the error would be printed in the console and still be caught by my error reporting tool

  try {
    throw new Error('FOO') // error that I don't want to block the main thread
  } catch (e) {
    console.log(e); // logging that works fine

    Meteor.setTimeout(() => {
      throw e; // error that is not handled as expected
    });
  }

Has anyone had any luck with this? We’ve seen the log message a lot recently on our server but can’t reproduce it locally so are a bit stumped as to how to find it. My hunch is that it emanates from the function dontBindEnvironment used by the Hook class.

Likely sources then might be DDP._reconnectHook and AccountsServer._validateLoginHook as they don’t specify debugPrintExceptions which would change the error printed. The reconnect hook is on the client though so that rules that out but we do have quite a lot of code added via Accounts.validateLoginAttempt . I’ve checked it though and can’t find anything async :thinking:

From what I’ve seen in development it can happen all over the place.

Http.get being one example, but it happens all over.

Yeah, we do have one HTTP.get inside our validateLoginHook but it’s not async…

Still searching :mag:

I’m trying to investigate this, and I’m very confused! It’s definitely coming from bindEnvironment. The problem is in:

onException = function (error) {
  Meteor._debug(
    "Exception in " + description + ":",
    error
  );
}

Even though error is a Meteor.Error object, when it is received by Meteor._debug() as the 2nd argument it is undefined (I tried replacing Meteor._debug to log the type of each argument).

However, if it is received as the 1st argument it is logged correctly. Very weird! Any ideas why that is?

I have replaced Meteor._bindEnvironment() with my own identical function, except for this bit:

onException = function (error) {
  Meteor._debug(
    "Exception in " + description + ":",
    error
  );
  if (typeof error === 'object') Meteor._debug(error); // <- I added this line
}

Now I get an error description and stack trace so I can track down the actual errors…

I would prefer to fix Meteor._debug so it can receive an Meteor.Error object as a 2nd parameter, but I don’t know why it’s not working :confused:

Edit: here’s a cleaner way of monkey-patching Meteor.bindEnvironment():

const defaultErrorHandler = function (error) {
  Meteor._debug("Exception in callback of async function:", error);
  if (typeof error === 'object') Meteor._debug(error);
}
const _origBindEnvironment = Meteor.bindEnvironment;
Meteor.bindEnvironment = function(func, onException, _this) {
  return _origBindEnvironment.call(this, func, onException || defaultErrorHandler, _this);
}

Might be something about console.log.apply not enumerating arguments properly:

Might be fixed with:

console.log.apply(console, Array.prototype.slice.call(arguments));

To ensure the arguments are array-ified before being applied to console.log?

Still gives “undefined”.

Within Meteor._debug if you do

console.log(allArgumentsOfTypeString, arguments.length, typeof arguments[1])
console.log(arguments[1])

you get

false 2 'undefined'
undefined

Or doing

Meteor._debug = function(a, b) {
  console.log(arguments.length, typeof b)  // 2 'undefined'
  console.log(b)                           // undefined

It’s really weird!

Wow, that is really weird :scream:

does Meteor._debug work normally outside a bindEnvironment?

Success!! I’ve tracked it down to (in my case) montiapm:agent which was imperfectly wrapping Meteor._debug(). Other APM agents based on Kadira might have the same problem. FYI @zodern

Edit: btw @zodern, none of these errors were being logged in MontiAPM because alreadyTracked is erroneously set to true. I think the assumption that “We’ve changed ‘stack’ into an object at method and sub handers…” is not true when Meteor._debug() is called within Meteor.bindEnvironment().

2 Likes

For sure Meteor really needs a better way to capture runtime errors that doesn’t require so many hacky ways to capture them.

Thanks @wildhart for finding the cause. I’ve checked the other common APM agents and they should be logging the error correctly. Tomorrow I will work on a fix for montiapm:agent.

btw @zodern, none of these errors were being logged in MontiAPM because alreadyTracked is erroneously set to true.

I think all of the apm agents have this problem. I will fix it tomorrow too in montiapm:agent.

1 Like

To be fair, the code in Meteor._debug exists entirely to address difference between older browsers. Here’s a selection of the comments:

// IE Companion breaks otherwise
// IE10 PP4 requires at least one argument
// IE 9 doesn't have console.log.apply, it's not a real Object.
// Most browsers
// Chrome and Safari only hyperlink URLs to source files in first argument
// IE9
// IE8

And outside Meteor, if you have an exception in a callback to an async function, you don’t get any extra information