1 of 22

JavaScript Async Contexts

Chengzhong Wu (Alibaba) 2020-06

2 of 22

Problems

🤯

3 of 22

Incomplete Error Stacks 🤒

TypeError: Failed to fetch

at rejectPromise

window.onload = e => {

fetch("https://no-exist.com").then(res => {

// doing something...

});

};

Error thrown in host provided async operations, like fetch API and net/http modules in Node.js, didn’t reflect the real cause location.

4 of 22

Where did we come to here? 🤔

export async function handler(ctx, next) {

const span = Tracer.startSpan();

// First query runs in the synchronous context of the request.

await dbQuery({ criteria: 'item > 10' });

// What about subsequent async operations?

await dbQuery({ criteria: 'item < 10' });

span.finish();

}

async function dbQuery(query) {

// How do we determine which request context we are in?

const span = Tracer.startSpan();

await db.query(query);

span.finish();

}

It can be very helpful to have ThreadLocal/AsyncLocal like features in JavaScript.

5 of 22

Leaking tasks 😵

test(() => {

// How to early fail the test with this leaking task?

setTimeout(() => {

throw new Error('foobar');

}, 1000);

});

// ✓ passed

test(async () => {

// How to early fail the test with this leaking task?

/** await */ asyncOperation();

});

// ✓ passed

- Early errors help debugging and testing strange behaviors and make applications easy to observe.

- Many JavaScript computing platforms also would like to know if all async tasks were resolved in an environment like Realms.

6 of 22

What are we solving?

  1. Hard to write/test/profile/instrument async applications.
  2. Hard to properly ensure when async code run ends.

7 of 22

Motivation

Ergonomically track async tasks in JavaScript

8 of 22

What is “async context”s

document.getElementById('button').onclick = e => {

// (1)

fetch("https://example.com").then(res => {

// (2)

return processBody(res.body).then(data => {

// (5)

const dialog = html`<dialog>Here's some cool data: ${data}

<button>OK, cool</button></dialog>`;

dialog.show();

dialog.querySelector("button").onclick = () => {

// (6)

dialog.close();

};

});

});

};

function processBody(body) {

// (3)

return body.json().then(obj => {

// (4)

return obj.data;

});

}

9 of 22

What is “async context”s

A simple plotting of the example async operation flow.

10 of 22

What is “async context”s

“Resource” can be multiplexed to request-responses on multiple async contexts.

11 of 22

Why can’t this be left to libraries?

  • async/await with promise-likes.
    • await { then: () => { /** broken contexts */ } };
  • It only works if all third-party libraries with custom scheduling call AsyncResource.runInAsyncScope(...).
  • We need to get library owners to think in terms of async context propagation.

12 of 22

Why can’t this be left to host environments?

  • Needs to be in place for platform environments to take advantage of it.
  • We need a standard API to get third-party libraries to work on different environments seamlessly.

13 of 22

Goals

  • Must be able to automatically link continuous async tasks.
  • Must not introduce implicit behavior on multiple tracking instance on single async flow.
  • Must provide a way to enable logical re-entrancy.
  • Should expose visibility into the async task scheduling and processing.
    • To perform cleanup or rendering or monitoring or test assertion steps.
    • Timing the total time spent in a zone, for analytics or in-the-field profiling.

14 of 22

Why no “error handling”?

  • Since async/await were introduced JavaScript, we can already use try..catch clause to catch asynchronous thrown errors.
  • Allowing a third party module author to intercept the exceptions of not lexically scoped code in a different module would be a hard to not introducing implicit behaviors.
  • The proposal is already a not small one. We’d like to keep a possibility to a future error handling on async flows proposal.

15 of 22

API Shapes

🚧 possible solution

16 of 22

No global accessing async local

class AsyncLocalStorage {

constructor();

enterWith(store: any);

exit();

getStore(): any;

}

Most exciting features built on top of async contexts tracking. “Global”s for async flows, like user interaction flow, http server request scope.

17 of 22

Use Case: timing

// tracker.js

const store = new AsyncLocalStorage();

export function start() {

// (a)

store.enterWith({ startTime: Date.now() });

}

export function end() {

// (b)

const dur = Date.now() - store.getStore().startTime;

console.log('onload duration:', dur);

store.exit()

}

import * as tracker from 'tracker.js'

window.onload = e => {

// (1)

tracker.start()

fetch("https://example.com").then(res => {

// (2)

return processBody(res.body).then(data => {

// (3)

const dialog = html`<dialog>Here's some cool data: ${data}

<button>OK, cool</button></dialog>`;

dialog.show();

tracker.end();

});

});

};

Simple re-entrancy implementation of calculating time consumed of a series of async operations without additional context.

18 of 22

API for Library Owners

Manual declaring an async resource which will trigger a series of async operations. Tracking outstanding third-party library with custom scheduling

class AsyncTask {

constructor(name);

get name;

runInAsyncScope(callback[, thisArg, ...args]);

}

19 of 22

Usage: multiplexing tasks

class DatabaseConnection {

constructor(port, host) {

// Initialize connection, possibly in root context.

this.socket = connect(port, host)

}

async query(search) {

const query = new Query(search)

const result = await this.socket.send(query)

// This context is triggered by `DatabaseConnection`

// which is not linked to initiator of `DatabaseConnection.query`.

return query.runInAsyncScope(() => {

// Promise linked to the initiator of `DatabaseConnection.query`.

// Promise -> Query(AsyncTask) -> `DatabaseConnection.query`

return Promise.resolve(result)

})

}

}

class Query extends AsyncTask {

constructor(search) {

// scheduled async task

super('database-query')

this.search = search

}

}

20 of 22

Hooks!

class AsyncHook {

constructor(hookSpec);

enable();

disable();

}

interface HookSpec {

scheduledAsyncTask(task, triggerTask);

beforeAsyncTaskExecute(task);

afterAsyncTaskExecute(task);

}

Tracking async tasks scheduling & executions

21 of 22

Usage: tasks tracking

const als = new AsyncLocalStorage();

const backlog = [];

const hook = new AsyncHook({

scheduledAsyncTask (task) {

const test = als.getStore();

if (test == null) {

return;

}

backlog.push(new WeakRef(task));

}

});

hook.enable();

als.enterWith(name);

try {

await callback();

} finally {

hook.disable();

als.exit();

}

assert(

backlog.filter(ref => ref.deref() != null).length === 0,

`'${name}' ended with dangling async tasks.`

);

Assert on no outstanding tasks out lives on end of the run

22 of 22

Next Steps

Stage 1: explore more possibilities of API shapes to provide explicit async flow management