Description
Async/await in Microvium
I'm not "promising" that async/await will be implemented in Microvium, but it's on my radar. This GitHub ticket is a consolidation of my personal design notes on the feature, in case anyone wants to review it and offer suggestions or improvements, or just for those interested in the process. Feel free to add comments to this ticket with any questions, corrections, clarifications or suggestions.
In this first message, I'll cover the motivation and requirements, and then in the next message, I'll cover the proposed design.
Why async/await?
- I believe that Microivum will be useful in IoT solutions, where a device interacts with a server. Async/await is a very ergonomic way of expressing control flow logic involving a non-local target.
- Similarly I've expressed that callback-based-async is a good reason to use Microvium in an otherwise-C environment, for expressing sequences of non-blocking behavior that might otherwise be expressed in C as complicated state machines. Obviously, async-await is even better than callbacks!
Objectives
- Be able to write
async
functions thatawait
the result of otherasync
functions. - Provide a way of creating something that an
async
function canawait
that isn't anotherasync
function. E.g. a thenable or promise. Ideally, this should be the syntaxnew Promise(...)
since this is the typical way to do this in JS. - As always, the behavior of user code should be the same in Microvium as in V8.
- As always, size is a major priority. First RAM size, and secondly the engine's compile size.
Gotchas to watch out for
What should the following print (in particular, in what order)?
useThenable('a');
usePromise('b');
useAsync('c');
async function useThenable(name) {
console.log(`Awaiting thenable ${name}`)
await { then: resolve => resolve() };
console.log(`Finished awaiting thenable ${name}`)
}
async function usePromise(name) {
console.log(`Awaiting promise ${name}`)
await new Promise(resolve => resolve());
console.log(`Finished awaiting promise ${name}`)
}
async function useAsync(name) {
console.log(`Awaiting async func ${name}`)
await (async () => {})();
console.log(`Finished awaiting async func ${name}`)
}
The answer is:
Awaiting thenable a
Awaiting promise b
Awaiting async func c
Finished awaiting promise b
Finished awaiting async func c
Finished awaiting thenable a
The key gotcha here is that the thenable
completes last. Why? Because a thenable is not a real Promise
, so the ECMA-262 spec says that it must be wrapped in a promise. I think the extra wrapping layer causes it to be posted to the promise job queue twice, so the callback is run last.
This also means that thenables are necessarily less efficient than promises, because thenables must be wrapped in promises anyway (so you land up having both the thenable and the promise in RAM) and they must have this behavior where they land up on the promise job queue twice instead of once.
Here's another gotcha:
usePromise('a');
useAsync('b');
useThenable('c');
function useThenable(name) {
console.log(`Waiting for thenable ${name}`)
waitFor({ then: resolve => resolve(name) });
}
async function usePromise(name) {
console.log(`Waiting for promise ${name}`)
waitFor(new Promise(resolve => resolve(name)));
}
async function useAsync(name) {
console.log(`Waiting for async func ${name}`)
waitFor((async () => name)());
}
function waitFor(thing) {
thing.then(result => {
console.log(`Finished waiting for ${result}`)
})
}
This prints:
Waiting for promise a
Waiting for async func b
Waiting for thenable c
Finished waiting for c
Finished waiting for a
Finished waiting for b
The key thing here is that the thenable c
completes first. Why? Because Promises always evaluate their callback asynchronously in the promise job queue, while an arbitrary thenable does not.
As stated in the objectives, if this code runs in Microvium then it must produce the same output. We have the choice either to forbid/remove certain features or to implement them correctly.