Skip to content

Instantly share code, notes, and snippets.

@takase1121
Created February 9, 2021 05:15
Show Gist options
  • Save takase1121/d48cb24a84ce34c42ec16eeca2cedae4 to your computer and use it in GitHub Desktop.
Save takase1121/d48cb24a84ce34c42ec16eeca2cedae4 to your computer and use it in GitHub Desktop.
Better stack trace for setInterval/setTimeout/event-loop invoked callbacks.

This is something that I do sometimes:

function a() {
  let i = 0
  function self() {
    if (i >= 5) {
      throw new Error("Oh no!")
    }
    console.log("tick!")
    i++
    setTimeout(self, 1000)
  }
  return setTimeout(self, 1000)
}

Of course I don't use it just to print tick!. In a sense, this is a recursive function (and has no tail-call limit, before you use it for that purpose please don't this is not suitable for that). The callback is queued to the event loop and executed later on another cycle. You can see the problem - the original call stack is effectively lost as it is now the event loop's job to call it.

The output:

❯ node t.js
tick!
tick!
tick!
tick!
tick!
C:\Users\Takase\Documents\stuffs\code\experiments\t.js:5
      throw new Error("Oh no!")
      ^

Error: Oh no!
    at Timeout.self [as _onTimeout] (C:\Users\Takase\Documents\stuffs\code\experiments\t.js:5:13)
    at listOnTimeout (internal/timers.js:554:17)
    at processTimers (internal/timers.js:497:7)

But what if there is a way to "preserve" the original call stack? There is, fortunately.

function b() {
  let i = 0
  let e = new Error()
  Error.captureStackTrace(e)
  function self() {
    if (i >= 5) {
      e.message = "Oh no!"
      throw e
    }
    console.log("tick!")
    i++
    setTimeout(self, 1000)
  }
  return setTimeout(self, 1000)
}

In here, we capture the stack trace onto a premade object and pass it to the callback. This means the stack trace is not generated when the event loop calls it, making the stack trace much more "readable"

Output:

❯ node t.js
tick!
tick!
tick!
tick!
tick!
C:\Users\Takase\Documents\stuffs\code\experiments\t.js:21
      throw e
      ^

Error: Oh no!
    at b (C:\Users\Takase\Documents\stuffs\code\experiments\t.js:17:9)
    at Object.<anonymous> (C:\Users\Takase\Documents\stuffs\code\experiments\t.js:32:1)
    at Module._compile (internal/modules/cjs/loader.js:1063:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1092:10)
    at Module.load (internal/modules/cjs/loader.js:928:32)
    at Function.Module._load (internal/modules/cjs/loader.js:769:14)
    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:72:12)
    at internal/main/run_main_module.js:17:47

Note: This is unsuitable for some use cases. For example, unwinding the stack repeatedly (in a loop) will be very inefficient. You also need to create the Error beforehand, but you probably can capture the stack on an object (that is allowed) and somehow "cast" the stack property onto your error later.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment