javascriptasync-await

Javascript does not execute code right after an awaited call immediately (it does other stuff in between) after the async function returned


I'll begin by showing what should be the expected output, both from a sensible view point and because it's what happens if the functions weren't async.

Edit: It is also what would happen if foo and bar received "done" callbacks instead of Promise.then callbacks.

about to resolve
about to return from foo
code right after awaited return from foo
about to return from bar
code right after awaited return from bar

But between returning from a function and executing the next line of code in the caller, Javascript is interleaving other code, resulting in the following:

about to resolve
about to return from foo
about to return from bar
code right after awaited return from foo
code right after awaited return from bar

I wonder if there's a way to prevent it. I need the next line of code in the caller to be executed right after the async function returns.

That's the output of the following code:

async function caller(cb) {
    await cb();
    console.log(`code right after awaited return from ${ cb.name }`);
}

const { promise, resolve } = Promise.withResolvers();

async function foo() {
    await promise;
    console.log('about to return from foo');
}

async function bar() {
    await promise;
    console.log('about to return from bar');
}

caller(foo);
caller(bar);

console.log('about to resolve');
resolve();


Solution

  • If the deepest awaited promise can be controlled, then it can be changed by a custom subclass of Promise that saves the prior then and invokes handlers in order, after previous handlers were completed.

    class SafePromise extends Promise {
        prior = Promise.resolve();
    
        then(onfulfilled, onrejected) {
            const prior = this.prior;
            const { promise, resolve } = Promise.withResolvers();
            this.prior = promise;
            return super.then(
                wrap(onfulfilled),
                wrap(onrejected)
            );
    
            function wrap(handler) {
                return async (_) => {
                    await prior;
                    try {
                        return await handler?.(_);
                    } finally {
                        resolve();
                    }
                };
            }
        }
    }
    

    But if the deepest promise is irremediably a native promise, then there really is no way to control the order of execution of await chains because the promise instance can't be changed to another promise class other than awaiting it, at which point the damage is already done because the then callback invocation is scheduled in a microtask.

    Javascript is also obtuse in adopting subclasses of Promise for async functions that await another subclass, it always just creates a native promise. The code to get back to awaiters might be something like this:

    function then() {
        return new Promise((resolve, reject) => {
            queueMicrotask(() => {
                promise.then(resolve, reject);
            });
        });
    }
    

    I can't think of a reason why it was designed this way, it doesn't resemble the ubiquitous call stack, which is the order the code executed and awaits were encountered in the first place. So the order up the chain is different from the order down the chain, it's not intuitive, and it's also inconsistent with "done" callbacks which is what promises aimed to substitute.

    In the second case, use a sync "done" callback that can be executed at the end of the async function simulating it returning as soon as possible.

    If an api that works with both cases is required, the awaited function must return a promise that never resolves when using the callback and return normally when not. Code in the callback and after await is repeated though.

    async function awaited(doneCallback) {
        // ...
        const result = await somePromise;
        if (doneCallback) {
            doneCallback(result); // simulate synchronous return
            return new Promise(() => {}); // and never resolve.
        }
        // otherwise return as expected of an await for this function.
        return result;
    }