Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Passing an async function as a callback causes the error stack trace to be lost

I'm trying to write a function that will reintroduce a stack trace when an object literal is thrown. (See this related question).

What I've noticed is that if a pass an async function as a callback into another async caller function, if the caller function has a try/catch, and catches any errors, and throws a new Error, then the stack trace gets lost.

I've tried several variants of this:

function alpha() {
  throw Error("I am an error!");
}

function alphaObectLiberal() {
  throw "I am an object literal!";  //Ordinarily this will cause the stack trace to be lost. 
}

function syncFunctionCaller(fn) {
  return fn();
}

function syncFunctionCaller2(fn) { //This wrapper wraps it in a proper error and subsequently preserves the stack trace. 
  try {
    return fn();
  } catch (err) {
    throw new Error(err); //Stack trace is preserved when it is synchronous. 
  }
}


async function asyncAlpha() {
  throw Error("I am also an error!"); //Stack trace is preseved if a proper error is thown from callback
}

async function asyncAlphaObjectLiteral() {
  throw "I am an object literal!"; //I want to catch this, and convert it to a proper Error object. 
}

async function asyncFunctionCaller(fn) {
  return await fn();
}

async function asyncFunctionCaller2(fn) {
  try {
    await fn();
  } catch (err) {
    throw new Error(err);
  }
}

async function asyncFunctionCaller3(fn) {
  try {
    await fn();
  } catch (err) {
    throw new Error("I'm an error thrown from the function caller!");
  }
}

async function asyncFunctionCaller4(fn) {
  throw new Error("No try catch here!");
}

async function everything() {
  try {
    syncFunctionCaller(alpha);
  } catch (err) {
    console.log(err);
  }


  try {
    syncFunctionCaller2(alphaObectLiberal);
  } catch (err) {
    console.log(err);
  }

  try {
    await asyncFunctionCaller(asyncAlpha);
  } catch (err) {
    console.log(err);
  }

  try {
    await asyncFunctionCaller2(asyncAlphaObjectLiteral);
  } catch (err) {
    console.log(err); //We've lost the `everthing` line number from the stack trace
  }

  try {
    await asyncFunctionCaller3(asyncAlphaObjectLiteral);
  } catch (err) {
    console.log(err); //We've lost the `everthing` line number from the stack trace
  }

  try {
    await asyncFunctionCaller4(asyncAlphaObjectLiteral);
  } catch (err) {
    console.log(err); //This one is fine
  }
}

everything();

(Code Sandbox)

Output: note my comments in the stack trace

[nodemon] starting `node src/index.js localhost 8080`
Error: I am an error!
    at alpha (/sandbox/src/index.js:2:9)
    at syncFunctionCaller (/sandbox/src/index.js:6:10)
    at everything (/sandbox/src/index.js:43:5) 
    //We can see what function caused this error
    at Object.<anonymous> (/sandbox/src/index.js:73:1)
    at Module._compile (internal/modules/cjs/loader.js:776:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:787:10)
    at Module.load (internal/modules/cjs/loader.js:653:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:593:12)
    at Function.Module._load (internal/modules/cjs/loader.js:585:3)
    at Function.Module.runMain (internal/modules/cjs/loader.js:829:12)
Error: I am an object literal!
    at syncFunctionCaller2 (/sandbox/src/index.js:17:11)
    at everything (/sandbox/src/index.js:65:5)
    //In a synchronous wrapper, the stack trace is preserved
    at Object.<anonymous> (/sandbox/src/index.js:95:1)
    at Module._compile (internal/modules/cjs/loader.js:776:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:787:10)
    at Module.load (internal/modules/cjs/loader.js:653:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:593:12)
    at Function.Module._load (internal/modules/cjs/loader.js:585:3)
    at Function.Module.runMain (internal/modules/cjs/loader.js:829:12)
    at startup (internal/bootstrap/node.js:283:19)
Error: I am also an error!
    at asyncAlpha (/sandbox/src/index.js:10:9)
    at asyncFunctionCaller (/sandbox/src/index.js:18:16)
    at everything (/sandbox/src/index.js:49:11) 
    //We can see what function caused this error
    at Object.<anonymous> (/sandbox/src/index.js:73:1)
    at Module._compile (internal/modules/cjs/loader.js:776:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:787:10)
    at Module.load (internal/modules/cjs/loader.js:653:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:593:12)
    at Function.Module._load (internal/modules/cjs/loader.js:585:3)
    at Function.Module.runMain (internal/modules/cjs/loader.js:829:12)
Error: I am an object literal!
    at asyncFunctionCaller2 (/sandbox/src/index.js:25:11) 
   //We've lost the stacktrace in `everything`
    at process._tickCallback (internal/process/next_tick.js:68:7)
    at Function.Module.runMain (internal/modules/cjs/loader.js:832:11)
    at startup (internal/bootstrap/node.js:283:19)
    at bootstrapNodeJSCore (internal/bootstrap/node.js:622:3)
Error: I'm an error thrown from the function caller!
    at asyncFunctionCaller3 (/sandbox/src/index.js:33:11)
    //We've lost the stacktrace in `everything`
    at process._tickCallback (internal/process/next_tick.js:68:7)
    at Function.Module.runMain (internal/modules/cjs/loader.js:832:11)
    at startup (internal/bootstrap/node.js:283:19)
    at bootstrapNodeJSCore (internal/bootstrap/node.js:622:3)
Error: No try catch here!
    at asyncFunctionCaller4 (/sandbox/src/index.js:38:9)
    at everything (/sandbox/src/index.js:67:11)
    //We can see what function caused this error
    at process._tickCallback (internal/process/next_tick.js:68:7)
    at Function.Module.runMain (internal/modules/cjs/loader.js:832:11)
    at startup (internal/bootstrap/node.js:283:19)
    at bootstrapNodeJSCore (internal/bootstrap/node.js:622:3)
[nodemon] clean exit - waiting for changes before restart

It seems to me that the await statement is what is screwing this up.

What's going on here?

like image 272
dwjohnston Avatar asked Jun 18 '19 08:06

dwjohnston


People also ask

What is the purpose of a callback in an asynchronous function?

A callback is just a function that's passed into another function, with the expectation that the callback will be called at the appropriate time. As we just saw, callbacks used to be the main way asynchronous functions were implemented in JavaScript.

Can a callback function be async?

Simply taking a callback doesn't make a function asynchronous. There are many examples of functions that take a function argument but are not asynchronous.

What is async stack traces?

Asynchronous stack traces allow you to inspect function calls beyond the current event loop. This is particularly useful because you can examine the scope of previously executed frames that are no longer on the event loop. This feature is currently an experiment and needs to be enabled.


1 Answers

Missing stack trace has nothing to do with Promises. Write the same code that has functions calling each other in synchronous way and you will observe exactly the same behavior i.e. loosing complete stack trace data when rethrowing new Error. It is only Error object that offers stack access. It in turn is backed by native code (like this of V8 engine) responsible for capturing stack trace of crossed stack frames. To make it worse each time you create Error object it captures stack from this point across the stackframes (at least it is observable in browser, nodejs implementation may differ). So that if you catch and retrow different Error object then its stack trace is visible on top of bubbling exception. Missing exceptions chaining for Error (no way to wrap new exception around caught one) makes it hard to fill these gaps. More interesting is that ECMA-262 spec chapter 19.5 does not introduce Error.prototype.stack property at all, in MDN in turn you find stack property is JS engine non-standard extension.

EDIT: Regarding missing "everything" function on stack it is side effect of how engine translates "async/await" into microtask calls and who is really calling specific callbacks. Refer to V8 engine team explanation as well as their zero-cost async stack traces document covering details. NodeJS starting from version 12.x will incorporate more cleaner stack traces, available with --async-stack-traces option offered by V8 engine.

like image 178
andy Avatar answered Oct 23 '22 11:10

andy