JavaScript's Missed Opportunity: Async/Await, Optional Chaining, and flatMap are the same thing
Someone asked me some time ago to share my thoughts on Async/Await in JavaScript. It is my belief that Async/Await (and to some extent*, Optional Chaining Syntax) is a missed opportunity in the evolution of the language. Consider the following three programs: const f = () => foo .? bar .? baz; const nullableResult = f(); const f = async () => { return await (await (await foo).bar).baz } const thennableResult = f(); const f = () => foo.flatMap(({bar}) => bar.flatMap(({baz}) => baz)); const manyResults = f(); In the first program, we assume that foo is a Nullable object with a bar property, that's a Nullable object with a baz property, that's a Nullable value. In the second, we assume that foo is a Thenable (also known as Promise) of an object with a bar property, that's a Thenable of an object with a baz property, that's a Thenable of a value. In the third, we assume that foo is a "Manyable" (also known as Array) of objects with bar properties, that are Manyables of objects with baz properties, that are Manyables of values. I'm sure you can already see the pattern here. But what if I told you that JavaScript could have evolved so that these three programs would be exactly the same, and work polymorphically on inputs of type Nullable*, Thenable, and Manyable? type Nullable = T | null; type Thenable = Promise; type Manyable = T[]; All that would be needed for that, is a common interface (like the Promise .then function) that the language syntax could build on (like they did with Async/Await). In fact, valid implementations of that potential interface already exist. It's just flatMap. We can also implement flatMap for Thenable and for Nullable: const Nullable = { flatMap(nullable, next){ return nullable === null ? null : next(nullable); } } const Thenable = { flatMap(thenable, next){ return thenable.then(value => next(value)); } } const Manyable = { flatMap(manyable, next){ return manyable.flatMap(value => next(value)); } } So we have our three flatMaps that have exactly the same interface. At this point, we're already able to create a function like the f we started with, but polymorphic (working on "anythingable" with a flatMap): const f = (Somethingable) => ( Somethingable.flatMap(foo, ({bar}) => ( Somethingable.flatMap(bar, ({baz}) => baz) )) ); ℹ️ Try it out with nullableResult = f(Nullable) If we'd associate these functions with the right prototypes, then we wouldn't even need to pass the correct interface implementation: Promise.prototype.flatMap = function(next){ return Thenable.flatMap(this, next); } // f now works on Arrays, and on Promises: const f = () => foo.flatMap(({bar}) => bar.flatMap(({baz}) => baz)); To some extent*, JavaScript could have adopted this approach, and given us a single syntax for flatMap that would be able to work with anything that's flat-mappable: > const foo = Promise.resolve({ bar: Promise.resolve({ baz: Promise.resolve("hello") }) }); > foo ?. bar ?. baz Promise > const foo = [{ bar: [{ baz: ["hello"] }] }]; > foo ?. bar ?. baz [ "hello" ] Or, like a more async-awaitey one: const f = flattening () => { const a = flatten foo; const b = flatten a.bar; return flatten b.baz; } Take a moment to let that sink in. This version of Async/Await looks exactly the same, but works on anything that's flattenable, not just Promises. It could have even integrated with custom types that have flatMap functions, like Observables, Iterables, Tasks, and whatnot. Instead, we're slowly getting a new special syntax for every new flatmappable type that's introduced to the language, in a process that's unfolding over the course of years, if not decades

Someone asked me some time ago to share my thoughts on Async/Await in JavaScript. It is my belief that Async/Await (and to some extent*, Optional Chaining Syntax) is a missed opportunity in the evolution of the language.
Consider the following three programs:
const f = () => foo .? bar .? baz;
const nullableResult = f();
const f = async () => { return await (await (await foo).bar).baz }
const thennableResult = f();
const f = () => foo.flatMap(({bar}) => bar.flatMap(({baz}) => baz));
const manyResults = f();
- In the first program, we assume that
foo
is a Nullable object with abar
property, that's a Nullable object with abaz
property, that's a Nullable value. - In the second, we assume that
foo
is a Thenable (also known as Promise) of an object with abar
property, that's a Thenable of an object with abaz
property, that's a Thenable of a value. - In the third, we assume that
foo
is a "Manyable" (also known as Array) of objects withbar
properties, that are Manyables of objects withbaz
properties, that are Manyables of values.
I'm sure you can already see the pattern here. But what if I told you that JavaScript could have evolved so that these three programs would be exactly the same, and work polymorphically on inputs of type Nullable
*, Thenable
, and Manyable
?
type Nullable<T> = T | null;
type Thenable<T> = Promise<T>;
type Manyable<T> = T[];
All that would be needed for that, is a common interface (like the Promise .then
function) that the language syntax could build on (like they did with Async/Await). In fact, valid implementations of that potential interface already exist. It's just flatMap
. We can also implement flatMap
for Thenable
and for Nullable
:
const Nullable = {
flatMap(nullable, next){
return nullable === null ? null : next(nullable);
}
}
const Thenable = {
flatMap(thenable, next){
return thenable.then(value => next(value));
}
}
const Manyable = {
flatMap(manyable, next){
return manyable.flatMap(value => next(value));
}
}
So we have our three flatMaps that have exactly the same interface. At this point, we're already able to create a function like the f
we started with, but polymorphic (working on "anythingable" with a flatMap
):
const f = (Somethingable) => (
Somethingable.flatMap(foo, ({bar}) => (
Somethingable.flatMap(bar, ({baz}) => baz)
))
);
ℹ️ Try it out with
nullableResult = f(Nullable)
If we'd associate these functions with the right prototypes, then we wouldn't even need to pass the correct interface implementation:
Promise.prototype.flatMap = function(next){
return Thenable.flatMap(this, next);
}
// f now works on Arrays, and on Promises:
const f = () => foo.flatMap(({bar}) => bar.flatMap(({baz}) => baz));
To some extent*, JavaScript could have adopted this approach, and given us a single syntax for flatMap
that would be able to work with anything that's flat-mappable:
> const foo = Promise.resolve({
bar: Promise.resolve({ baz: Promise.resolve("hello") })
});
> foo ?. bar ?. baz
Promise<"hello">
> const foo = [{ bar: [{ baz: ["hello"] }] }];
> foo ?. bar ?. baz
[ "hello" ]
Or, like a more async-awaitey one:
const f = flattening () => {
const a = flatten foo;
const b = flatten a.bar;
return flatten b.baz;
}
Take a moment to let that sink in. This version of Async/Await looks exactly the same, but works on anything that's flattenable, not just Promises. It could have even integrated with custom types that have flatMap
functions, like Observables, Iterables, Tasks, and whatnot. Instead, we're slowly getting a new special syntax for every new flatmappable type that's introduced to the language, in a process that's unfolding over the course of years, if not decades