Barnash
Barnash

Reputation: 2042

How can I see the full stack trace of error in setTimeout with a promise

I'm trying to understand how can I get a full stack trace from a promise rejection caused by a setTimeout

I'm running the following example:

'use strict';

function main() {
  f1().catch(e => {
    console.error('got error with trace:');
    console.error(e);
  });

  f2().catch(e => {
    console.error('got error with trace:');
    console.error(e);
  });
}

async function f1() {
  return new Promise((resolve, reject) => {
    reject(new Error('Error in normal flow'));
  });
}

async function f2() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject(new Error('Error in timeout'));
    }, 0);
  });
}

main();

And I'm getting this output:

got error with trace:
Error: Error in normal flow
    at Promise (/Users/me/project/example.js:25:12)
    at Promise (<anonymous>)
    at f2 (/Users/me/project/example.js:24:10)
    at main (/Users/me/project/example.js:9:3)
    at Object.<anonymous> (/Users/me/project/example.js:29:1)
    at Module._compile (module.js:569:30)
    at Object.Module._extensions..js (module.js:580:10)
    at Module.load (module.js:503:32)
    at tryModuleLoad (module.js:466:12)
    at Function.Module._load (module.js:458:3)
got error with trace:
Error: Error in timeout
    at Timeout.setTimeout [as _onTimeout] (/Users/me/project/example.js:18:14)
    at ontimeout (timers.js:488:11)
    at tryOnTimeout (timers.js:323:5)
    at Timer.listOnTimeout (timers.js:283:5)

How can I make make the stack trace of the promise that is initiated with setTimeout be more verbose like the promise without setTimeout?

When this happen to me in real production code, I can't know exactly where the error was initiated from. Which makes it very difficult to debug.

Upvotes: 2

Views: 1814

Answers (4)

Kirill Groshkov
Kirill Groshkov

Reputation: 1771

I've solved it like this: https://github.com/NaturalCycles/js-lib/blob/master/src/promise/pTimeout.ts

pTimeout function code:

export async function pTimeout<T>(promise: Promise<T>, opt: PTimeoutOptions): Promise<T> {
  const { timeout, name, onTimeout, keepStackTrace = true } = opt
  const fakeError = keepStackTrace ? new Error('TimeoutError') : undefined

  // eslint-disable-next-line no-async-promise-executor
  return await new Promise(async (resolve, reject) => {
    // Prepare the timeout timer
    const timer = setTimeout(() => {
      const err = new TimeoutError(`"${name || 'pTimeout function'}" timed out after ${timeout} ms`)
      if (fakeError) err.stack = fakeError.stack // keep original stack
      reject(err)
    }, timeout)

    // Execute the Function
    try {
      resolve(await promise)
    } catch (err) {
      reject(err)
    } finally {
      clearTimeout(timer)
    }
  })
}

Upvotes: 0

Chet
Chet

Reputation: 19849

There are some decent answers in here, but on thing that hasn't been considered is that generating a stack trace is expensive and you don't want to do it all the time. V8 has a fancy JIT compiler that can run things out of order and has to be unwound to get a proper stack trace. Thus we should only generate the stack trace for the "potential error" if and when we need it.

async function f2() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            // Stack trace is unhelpful here.
            reject(new Error("Error in timeout"))
        }, 0)
    })
}

async function f3() {
    // This stack trace is useful here, but generating a stack trace is expensive
    // because V8 has to unravel all of its fancy JIT stuff.
    const potentialError = new Error("Error in timeout")
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            reject(potentialError)
        }, 0)
    })
}

async function f4() {
    try {
        const result = await new Promise((resolve, reject) => {
            setTimeout(() => {
                reject(new Error("Error in timeout"))
            }, 0)
        })
        return result
    } catch (error) {
        // Override the stack trace only when you need it.
        error.stack = new Error().stack
        throw error
    }
}

Upvotes: 1

Bergi
Bergi

Reputation: 664630

I'd try to write it like this:

async function f() {
  await new Promise((resolve, reject) => {
    setTimeout(resolve, 0);
  });
  throw new Error('Error after timeout');
}

Try to avoid doing anything in non-promise callbacks.

Upvotes: 1

carpeliam
carpeliam

Reputation: 6769

I've done something like the following before:

async function f2() {
  return new Promise((resolve, reject) => {
    const potentialError = new Error('Error in timeout');
    setTimeout(() => {
      reject(potentialError);
    }, 0);
  });
}

That'll result in an error just like the one outside setTimeout. Wherever you create your error, that'll dictate your stack trace.

One use case for this for me has been a test that times out because a promise never resolves (in my case, with puppeteer and mocha/jasmine). Because the timeout had no meaningful stack trace, I wrote a wrapper around the promise that includes a setTimeout, much like this example.

Upvotes: 1

Related Questions