Search code examples
javascriptnode.jsv8

try/catch causes <anonymous> in stack


If I have a try/catch in one of my calls and it causes the stack to "reset" at that point. I have 2 questions because of this -

1) Why does this happen? I imagine it's something with how the v8 engine works but it would be interesting to know why.

2) Is there a good solution to use async/await and still keep the entire stack trace? Right now I am putting a try/catch all the way down the function call chain and rolling an error into a new error all the way back out (using VError).

The following code gives the stack trace I would expect

async function one() {
  throw new Error("blah");
}

async function two() {
  await one();
}

async function three() {
  await two();
}

async function four() {
  try {
    await three();
  } catch (e) {
    console.log(e);
  }
}

four();

stack trace

Error: blah
  at one (/dev/async-stack/correct-stack.js:2:9)
  at two (/dev/async-stack/correct-stack.js:6:9)
  at three (/dev/async-stack/correct-stack.js:10:9)
  at four (/dev/async-stack/correct-stack.js:15:11)
  at Object.<anonymous> (/dev/async-stack/correct-stack.js:21:1)
  at Module._compile (module.js:652:30)
  at Object.Module._extensions..js (module.js:663:10)
  at Module.load (module.js:565:32)
  at tryModuleLoad (module.js:505:12)
  at Function.Module._load (module.js:497:3)

Putting in a try/catch in the middle causes a stack trace to start where the last try/catch was.

async function one() {
  throw new Error("blah");
}

async function breaker() {
  return true;
}

async function stack() {
  try {
    await breaker();
  } catch (error) {
    throw error;
  }
}

async function two() {
  await stack(); // <-- this call 
  await one();
}

async function three() {
  await two();
}

async function four() {
  try {
    await three();
  } catch (e) {
    console.log(e);
  }
}

four();

stack trace

Error: blah
  at one (/dev/async-stack/broken-stack.js:2:9)
  at two (/dev/async-stack/broken-stack.js:19:9)
  at <anonymous>
  at process._tickCallback (internal/process/next_tick.js:188:7)
  at Function.Module.runMain (module.js:695:11)
  at startup (bootstrap_node.js:188:16)
  at bootstrap_node.js:609:3

Solution

  • That's how async/await syntax desugars. For your first snippet, it's like1

    function one() {
      return Promise.reject(new Error("blah"));
    }
    
    function two() {
      return one().then(() => {});
    }
    
    function three() {
      return two().then(() => {});
    }
    
    function four() {
      return three().then(() => {}).catch(e => { console.log(e); });
    }
    
    four();
    

    while your second snippet works like1

    function one() {
      return Promise.reject(new Error("blah"));
    }
    
    function breaker() {
      return Promise.resolve(true);
    }
    
    function stack() {
      return breaker().then(() => {}).catch(error => { throw error; });
    }
    
    function two() {
      return stack().then(() => {
    //                    ^^^^^^^ this anonymous function
        return one().then(() => {});
      })
    }
    
    function three() {
      return two().then(() => {});
    }
    
    function four() {
      return three().then(() => {}).catch(e => { console.log(e); });
    }
    
    four();
    

    As you can see, one() is indeed called from inside an anonymous then callback. It actually doesn't have anything to do with try/catch as your title suggests, but rather that any await precedes the one() call.

    1: Ignoring details such as Promise constructor calls, which probably use a deferred pattern internally so that they don't show up in the stack traces. A more pedantic way would be to write function() { var _resolve, _reject, _promise = new Promise((res, rej) => { _resolve = res; _reject = rej; }); try { /* function body */ _resolve(_return_value); } catch(e) { _reject(e); } return _promise; }