Any way to detect an async stack trace (like Chrome devtools does)?

# #!/JoePea (4 months ago)

Is there some way (perhaps by patching methods on objects?) so we can track async call stacks?

When we pause code in devtools, we are able to see the async stack trace of the code.

What I'd like to do is to effectively detect the same thing as devtools does at some point in any code. As a specific example, I'd like to detect the async stack trace of any call to fetch.

Is such a thing possible with runtime code?

Or would it require instrumentation of the source code?

#!/JoePea

# kai zhu (4 months ago)

here's a simple throwaway-function you can wrap around promises like fetch to get the caller's async-stack-trace promiseWithErrorStack:

<!doctype html>
<html lang="en">
<body>
<h1>test.html</h1>
<script>
(async function foo() {
    function promiseWithErrorStack(promise) {
    /*
     * this function will append current-stack to any err caught from
<promise>
     */
        let errStack;
        errStack = new Error().stack;
        return new Promise(function (resolve, reject) {
            promise.then(resolve).catch(function (err) {
                // append current errStack to err.stack
                if (err && typeof err.stack === "string") {
                    err.stack += "\n" + errStack;
                }
                reject(err);
            });
        });
    }
    await promiseWithErrorStack(fetch("https://example.com")); // at foo
(test.html:23)

    /*
    console-output:

    Uncaught (in promise) TypeError: Failed to fetch
    Error
        at promiseWithErrorStack (test.html:12)
        at foo (test.html:23) // async-stack-trace
        at test.html:32
    */
}());
</script>
</body>
# #!/JoePea (4 months ago)

Thanks for the idea! That's similar to what I thought would be necessary. I was hoping to monkey patch fetch() to have it get the trace if any fetch call, but they would not be async stack traces. For async traces it would require instrumentation by injecting code similar to yours (I want to detect traces in third-party code installed locally).

#!/JoePea

# kai zhu (4 months ago)

(I want to detect traces in third-party code installed locally).

  1. here's 15-line javascript-hack to trace async-fetch-calls from 3rd-party-cdn-library
<!doctype html>
<html lang="en">
<body>
<h1>test.html</h1>
<script>
(function () {
/*
 * 15-line-javascript-hack to trace async-fetch-calls
 * enable hack by adding search-query "?modeDebugFetch=1" to web-url
 */
    "use strict";
    if ((/\bmodeDebugFetch=1\b/).test(location.search)) {
        let fetch0;
        fetch0 = globalThis.fetch;
        globalThis.fetch = function (...argList) {
            let errStack = new Error().stack;
            console.error("\n\nfetch-call-arguments:");
            console.error(JSON.stringify(argList, undefined, 4));
            console.error("\n\nfetch-call-trace:");
            console.error(errStack);
            return fetch0(...argList);
        };
    }
}());
</script>
<!-- 3rd-party-cdn-library -->
<script src="https://unpkg.com/http-client/umd/http-client.js"></script>
<script>
(async function foo() {
    "use strict";
    let thirdParty = window.HTTPClient;
    let myFetch = thirdParty.createFetch(
        thirdParty.base("https://api.stripe.com/v1"),
        thirdParty.accept("application/json")
    );
    let response = await myFetch("/customers/5");
    console.log(response.jsonData);
/*
dev-console-output - http://localhost:8081/test.html?modeDebugFetch=1
fetch-call-arguments:
[
    "https://api.stripe.com/v1/customers/5",
    {
        "headers": {
            "Accept": "application/json"
        },
        "responseHandlers": [
            null
        ]
    }
]
fetch-call-trace:
Error
    at globalThis.fetch (test.html?modeDebugFetch=1:16)
    at http-client.js:194
    at http-client.js:127
    at http-client.js:217
    at http-client.js:126
    at http-client.js:154
    at http-client.js:95
    at foo (test.html?modeDebugFetch=1:35)
    at test.html?modeDebugFetch=1:64
*/
}());
</script>
</body>
  1. here's real-world-hack added to npm-cli.js to debug its http-requests
C:\Program Files\nodejs\node_modules\npm>git diff

diff --git a/bin/npm-cli.js b/bin/npm-cli.js
index 561dec0..98cafb8 100644
--- a/bin/npm-cli.js
+++ b/bin/npm-cli.js
@@ -2,17 +2,6 @@
 ;(function () { // wrapper in case we're in module_context mode
+// hack - debug http-request
+let httpRequest;
+httpRequest = require("https").request.bind(require("https"));
+require("https").request = function (...argList) {
+    if (process.env.NPM_DEBUG) {
+        console.error(
+            "npm - httpRequest - " + JSON.stringify(argList.slice(0, 2),
undefined, 4)
+        );
+    }
+    return httpRequest(...argList);
+};
   // windows: running "npm blah" in this folder will invoke WSH, not node.

C:\Program Files\nodejs\node_modules\npm>
$ NPM_DEBUG=1 npm publish foo
# console-ouput:
# npm - httpRequest - [
#     {
#         "protocol": "https:",
#         "href": "https://registry.npmjs.org/foo",
#         "method": "GET",
#         "headers": {
#             "connection": [
#                 "keep-alive"
#             ],
#             "user-agent": [
#                 "npm/6.13.4 node/v12.16.1 win32 x64"
#             ],
#             ...
# #!/JoePea (4 months ago)

Hello Kai! That example is so meta with its own output showing the numbers relative to the code including its own output! :)

That's what I originally wanted to do, but that doesn't give us an async stack trace, it only gives a sync stack trace (I presume within the current event loop task).

For example:

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()

Output:

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

Live example:

codepen.io/trusktr/pen/b8fd92752b4671268f516ad3804869e4?editors=1010

I opened a request for this over here: tc39/proposal-error-stacks#34

#!/JoePea

# Jacob Bloom (4 months ago)

Reading through the issue JoePea linked to, it looks like the difficulties with standardized async stack traces are twofold:

  1. Even with the error stacks proposal, the implementer has total say over what qualifies as a stack frame
  2. The way promises are chained means that some patterns like async loops would theoretically unroll to very long stack traces, and right now engines aren't required to keep track of those frames; requiring them to do so would place a burden on implementers that could quickly lead to slowdowns and memory issues

What if there was a standard way to mark a Promise as an important stack frame, which the implementer is free to ignore? Maybe something like Promise.prototype.trace()

# #!/JoePea (3 months ago)

The way promises are chained means that some patterns like async loops would theoretically unroll to very long stack traces

Devtools in fact does that (has a button at the bottom of the stack to keep loading more frames).

I think it would be better for it to be smart and not have duplicate frames in the stack, so a loop would result in only one unique set of entries in the stack trace and not repeat.

#!/JoePea