proposal-error-stacks icon indicating copy to clipboard operation
proposal-error-stacks copied to clipboard

async stack traces

Open trusktr opened this issue 3 years ago • 13 comments

It would be convenient to get async stack traces, like what Chrome Devtools shows us in the sources tab.

F.e.

// main.js
function foo() {
  setTimeout(function bar() {
    const stack = getAsyncStackString()
    console.log(stack)
  }, 1000)
}
foo()

/*
Eventually logs the trace
  at bar (main.js:4)
  at foo (main.js:3)
  (main.js:7)
*/

Or similar; not sure if I got that right but you get the idea.

Use case:

I'd like to detect the async stack trace of any fetch call in an app (including 3rd-party modules from node_modules).

I would like to only have to monkey-patch fetch() so that in each call I can have it get the stack trace. However this only gives non-async traces.

To achieve this currently, I would need to instrument the code which is more complicated.

trusktr avatar Jul 10 '20 08:07 trusktr

Can this be solved by loading a custom Promise library that supports long stack traces? IIRC bluebird advertized this as one of its features.

mk-pmb avatar Jul 10 '20 13:07 mk-pmb

The content of stack traces is up to the implementation. I'm not really clear on what you're asking for here.

ljharb avatar Jul 11 '20 00:07 ljharb

Can this be solved by loading a custom Promise library that supports long stack traces? IIRC bluebird advertized this as one of its features.

@mk-pmb How do I tell fetch to return a custom Promise that isn't the one built into the browser? Does fetch rely on the Promise global?

The content of stack traces is up to the implementation. I'm not really clear on what you're asking for here.

@ljharb I thought the purpose of this proposal was to make it not up to the implementation, but up to a standard.

What I'm hoping for is a function that returns a full async stack trace, not just a synchronous one in the current task.

For example, the output of the following,

const originalFetch = globalThis.fetch

globalThis.fetch = function(...args) {
	const stack = new Error().stack
	console.log(stack)
	return originalFetch.apply(globalThis, args)
}

const sleep = t => new Promise(r => setTimeout(r, t))

async function one() {
	console.log('1')
	await sleep(10)
	two()
}

async function two() {
	console.log('2')
	await sleep(10)
	three()
}

async function three() {
	console.log('3')
	await sleep(10)
	return await fetch('https://unpkg.com/[email protected]')
}

async function main() {
	await one()
}

main()

is

1
2
3
Error
    at globalThis.fetch (pen.js:5)
    at three (pen.js:27)

but I was hoping it would be more like

1
2
3
Error
    at globalThis.fetch (pen.js:5)
    at three (pen.js:27)
    at two (...)
    at one (...)
    at main (...)

Live example: https://codepen.io/trusktr/pen/b8fd92752b4671268f516ad3804869e4?editors=1010

trusktr avatar Jul 11 '20 04:07 trusktr

The purpose of this proposal is to specify the structure, not the contents, which will remain implementation-defined.

ljharb avatar Jul 11 '20 05:07 ljharb

Then as I understand the original request, it is out of scope of this spec.

mk-pmb avatar Jul 11 '20 14:07 mk-pmb

out-of-loop-traces are expensive to bookkeep. i could envision performance-hit to programs written by someone naively using promises/await-calls at every opportunity with this feature.

we could instead standardize entry-point-traces for a few debug-use-cases where it makes sense:

  1. tracking-down entry-point for http-requests. relevant bodies/nodejs could possibly hash-out standard to preserve entry-point-stack in XMLHttpRequest.prototype.send, fetch, and node's [http/https/http2].request

  2. tracking-down entry-point for Promise.all, Promise.race. similar to 1, with standards-body being tc39.

  3. tracking-down entry-point where event/message was emitted. there's nothing a standards body can do here. its up to userland to include stack-trace in emitted message-payload.

standardizing 1 and 2 might seem like warts to language-purists, but they're outsized impact to debugging could justify it.

kaizhu256 avatar Jul 11 '20 17:07 kaizhu256

tracking-down entry-point for http-requests.

This sounds arbitrary and overly specific. Why should one special method of communication receive more attention than others, e.g. reading from standard input, or a window manager's notification received by a GUI application?

mk-pmb avatar Jul 11 '20 17:07 mk-pmb

if you can do empirical-survey of what javascript-entry-points people find most-useful-to-know when debugging, i'm all for it.

i don't have insight in desktop-development. my insight in web-development however, is that http-request-entry-points is at top-of-the-list.

providing a standard-trace for http-requests would also prevent me from doing well-intentioned-but-stupid-things like:

let originalFetch = globalThis.fetch;
globalThis.fetch = function (...argList) {
    console.error(new Error().stack);
    return originalFetch(...argList);
};

that could end up being security risks.

kaizhu256 avatar Jul 11 '20 17:07 kaizhu256

The spec should have reasons that make sense independent of popular vote and fashions. We should build mechanisms that empower all developers to more easily debug whatever they're working on. In your fetch example case one solution might be to have browser vendors implement flags to enable a security-preserving version of long stack traces on parts of their API.

mk-pmb avatar Jul 11 '20 18:07 mk-pmb

While I agree with the OP on this, I understand that this proposal is about form and not content. On that note, can there not be an another accessor property with the same setup as stack, but guaranteed to return a complete-with-async-frames stack trace? The trace returned by stack can remain as it is, with content decided by the vendors, but there does need to be a guaranteed approach to getting a complete stack trace that's not missing the async frames.

I have run into issues because of situations like this. For instance, in Firefox, as long as the developer tools are open, some code with functionality that probes the stack trace works as intended. However, without the developer tools, the same code breaks because they stop adding the async frames to the stack trace.

rdking avatar Jun 21 '21 20:06 rdking

can there not be an another accessor property with the same setup as stack, but guaranteed to return a complete-with-async-frames stack trace?

I see no obstacle. Someone should make a proposal for that.

I'm not sure I understood the example though. Whether and in which circumstances a specific web browser does things one way or another, seems to be a product-specific problem.

mk-pmb avatar Jul 01 '21 21:07 mk-pmb

@mk-pmb it can not be an accessor property, only a static method, so it can be shadowed per-compartment. .stack, when specified, will be marked "legacy" so that it can be absent in spec-compliant engines.

ljharb avatar Jul 01 '21 23:07 ljharb

I'd be happy even if it were as static method. Just so long as there is a consistent way to get it, that's good enough.

rdking avatar Jul 08 '21 18:07 rdking