# console.createTask API Chrome supports automatic tracking of stack traces through asynchronous operation. For example, the following code: ```javascript async function f1() { setTimeout(function f2() { console.trace("hi!") }); } function root() { f1(); } root(); ``` will show the following stack trace: ``` f2 @ VM115:5 setTimeout f1 @ VM115:4 root @ VM115:8 (anonymous) @ VM115:11 ``` To allow custom schedulers to show up in these "stitched together stack traces of the async flow", Chrome exposes a `console.createTask(name: string).run(fn: Function)` API which captures the current stack trace when you call `console.createTask`, and appends it to stack traces generated by async flows that continue from the `.run` callback. For example, this code: ```javascript function f1() { setTimeout(function f2() { console.trace("hi!") }); } let task; function setupTask() { task = console.createTask("MY TASK!"); } function root() { task.run(f1); } setupTask(); root(); ``` will log ``` f2 @ VM677:3 setTimeout f1 @ VM677:2 root @ VM677:13 (anonymous) @ VM677:17 MY TASK setupTask @ VM677:9 (anonymous) @ VM677:16 ``` Effectively, built-in schedulers like `setTimeout` are implemented as if they were doing: ```javascript function setTimeout(callback, time) { const task = console.createTask("setTimeout"); __internalSetTimeout(() => { task.run(callback); }, time) } ``` ## `console.createTask` and `AsyncContext` Given that `console.createTask` "tracks async flow spawned by the .run" callback, it could be implemented on top of `AsyncContext`: ```javascript const __currentTaskTrace = new AsyncContext.Variable({ defaultValue: "" }); function __addTrace(prefix) { const stack = new Error().stack.split("\n").slice(3).join("\n") return `${prefix}\n${stack}${__currentTaskTrace.get()}` } const cool_console = { trace(message) { console.log(__addTrace(message)); }, createTask(name) { const taskTrace = __addTrace(`\n at ${name}`); return { run: fn => __currentTaskTrace.run(taskTrace, fn) }; } }; ``` ## `console.createTask` and events One of the most difficult question to answer when it comes to `AsyncContext` is what to do when it comes to events: is the logical code flow continuing from where the event listener was registered, or from where some code caused the event to be dispatched? `console.createTask` seems to have different answers depending on the event. ### `AbortSignal`'s `abort` (triggered synchronously by JS) ```javascript let ac = new AbortController(); function register() { ac.signal.addEventListener("abort", () => console.trace("hi")); } function dispatch() { ac.abort(); } console.createTask("register").run(register); console.createTask("dispatch").run(dispatch); ``` The logged trace contains only **dispatch**. ### `HTMLElement`'s `click` (either triggered synchronously by JS, or by user interaction) ```javascript function register() { document.addEventListener("click", () => console.trace("hi")); } function dispatch() { document.body.click(); } console.createTask("register").run(register); console.createTask("dispatch").run(dispatch); // now, also click manually and see the second trace ``` The first logged trace contains only **dispatch**. The logged trace when manually clicking contains _neither_. ### `Window`'s `message` (triggered asynchronously by JS) ```javascript function register() { window.addEventListener("message", () => console.trace("hi")); } function dispatch() { window.postMessage("hi"); console.log("after dispatch") } console.createTask("register").run(register); console.createTask("dispatch").run(dispatch); ``` The logged trace contains **dispatch**, which has a browser-injected **postMessage** task as a child. ### `HTMLStyleElement`'s `load` (triggered asynchronously by JS) ```javascript const style = document.createElement("style"); function register() { style.addEventListener("load", () => console.trace("hi")); } function dispatch() { document.head.appendChild(style) } console.createTask("register").run(register); console.createTask("dispatch").run(dispatch); ``` The logged trace contains **register**, which has a browser-injected **load** task as a child. ### `HTMLStyleElement`'s `load` (triggered synchronously through `dispatchEvent`) ```javascript const style = document.createElement("style"); function register() { style.addEventListener("load", () => console.trace("hi")); } function dispatch() { style.dispatchEvent(new Event("load")) } console.createTask("register").run(register); console.createTask("dispatch").run(dispatch); ``` The logged trace contains **register**, which has a browser-injected **load** task as a child. ### Custom `foo` on `HTMLStyleElement` (triggered synchronously through `dispatchEvent`) ```javascript const style = document.createElement("style"); function register() { style.addEventListener("foo", () => console.trace("hi")); } function dispatch() { style.dispatchEvent(new Event("foo")) } console.createTask("register").run(register); console.createTask("dispatch").run(dispatch); ``` The logged trace contains only **dispatch**. ### `load` on a custom `EventTarget` instance (triggered synchronously through `dispatchEvent`) ```javascript const et = new EventTarget(); function register() { et.addEventListener("load", () => console.trace("hi")); } function dispatch() { style.dispatchEvent(new Event("load")) } console.createTask("register").run(register); console.createTask("dispatch").run(dispatch); ``` The logged trace contains **register**, which has a browser-injected **load** task as a child. ### `foo` on a custom `EventTarget` instance (triggered synchronously through `dispatchEvent`) ```javascript const et = new EventTarget(); function register() { et.addEventListener("foo", () => console.trace("hi")); } function dispatch() { style.dispatchEvent(new Event("foo")) } console.createTask("register").run(register); console.createTask("dispatch").run(dispatch); ``` The logged trace contains only **dispatch**. ## Conclusion By default, events propagate the current task from the dispatch context, falling back to the root context if the event has been dispatched by the browser. However for some events, such as `"load"`, they propagate the current task from the registration context. Whether this happens depends only on the event name, and not on the EventTarget instance or on how the event is dispatched. That `"load"` and `"error"` are the only events for which the registration context is used, as defined at https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/dom/events/event_target.cc;drc=7df47a37470130cdf03467448a1f9d0d228fc47a;l=114. ### Implementing `console.createTask` on top of `AsyncContext` To re-explain the `console.createTask` propagation on top of `AsyncContext`, we must use the dispatch target for event listeners. `EventTarget` must special-case `"error"` and `"load"` to either propagate the full registration context, or to only propagate the the registration-time value relative to `console.createTask`. I would recommend the second, to avoid `"error"`/`"load"` behaving "weirdly" in the general case. An `.addEventListener` would look as follows, based on the above `cool_console` example: ```javascript class EventTarget { #callbacks = new Map(); addEventListener(name, callback) { // https://github.com/tc39/proposal-upsert const callbacks = this.#callbacks.emplace(name, []); if (name === "load" || name === "error") { callbacks.push({ callback, currentTask: __currentTaskTrace.get() }); } else { callbacks.push({ callback, currentTask: undefined }); } } dispatchEvent(e) { const callbacks = this.#callbacks.get(e.name) ?? []; const defaultTask = __currentTaskTrace.get(); for (const { callback, currentTask = defaultTask } of callbacks) { __currentTaskTrace.run(currentTask, callback.bind(null, e)); } } } ``` Which is similar to how it's already implemented at https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/dom/events/event_target.cc;drc=4b44bd63028a5aa82976dc4d7ed51cb78cbc6891;l=677.