My issue with async/await specifically, and promises generally is that it blurs the line between a value and a value object. If you think about Promise { value }
it becomes very important whether a function returns a value or a promise wrapped value. Unfortunately, JS promises don't behave like category theory objects (so much for the myriad of Promises are Monads
blogposts) and will resolve nested values until done or rejected. This can make reasoning about their behaviour tricky and makes writing types for their functions tougher as well.
Let's start with my ideal in JS, just using promises:
/* pretend these functions are network calls, hence the promise.resolve */
const producesA = () => Promise.resolve(1);
const producesB = a => Promise.resolve(a + 1);
const doSomething = (a, b) => Promise.resolve(a + b);
function foo() {
const a = producesA();
const b = a.then(producesB);
return Promise.all([a, b])
.then(([a, b]) => doSomething(a, b));
}
any local error handling can be performed by adding a .catch to any of the methods, and global to the invocation of the function. It's clear at every step what is a value and what is a value object.
async function foo() {
const a = await producesA();
const b = await producedB(a);
return doSomething(a, b);
}
it's not clear whether doSomething is a function that returns a promise or a value. so it's not obvious whether I should await it, or not. nor is it clear whether to catch it or not. if we insist that the function foo returns a promise, this is no longer a problem:
async function foo(): Promise<int> {
const a = await producesA();
const b = await producedB(a);
return doSomething(a, b); // must return an int, otherwise it would return Promise<Promise<int>> which would break the compiler
}
this is obviously not the way promises work in practice (since they are resolved all the way down). but you can pretend they work this way with reason, which will likely have a performance impact, but you don't have to worry about safety:
/* pretend these functions are network calls */
let producesA () => Js.Promise.resolve 1;
let producesB a => Js.Promise.resolve (a + 1);
let doSomething x y => Js.Promise.resolve (x + y);
let foo () :Js.Promise.t int => {
let a = producesA ();
let b = Js.Promise.then_ producesB a;
Js.Promise.then_ (fun (v1, v2) => doSomething v1 v2) (Js.Promise.all2 (a, b))
};
if we imagine an async await syntax for reason, where let%await
is the equivalent of const x = await foo()
, then we could have
let producesA () => Js.Promise.resolve 1;
let producesB a => Js.Promise.resolve (a + 1);
let doSomething x y => Js.Promise.resolve (x + y);
let foo () :Js.Promise.t int => {
let%await a = producesA ();
let%await b = producesB a;
doSomething a b;
}
and we know that doSomething must be a promise that returns an int.