Notes

Tips for my future self

Note #7: Nested Promises in JavaScript

Jun 09, 19

In JavaScript, nested promises will automatically be flattened by .then and await: await Promise.resolve(Promise.resolve(5)) === 5. This will cause issues with asynchronous methods taking generic values; as they will behave different if they take a Promise than with any other object.

For example, take the following method:

1
2
3
async function identity(t) {
    return t;
}

With this method, we would expect the following assertion to always be truthy:

1
alert(obj === await identity(obj));

And that is the case… unless obj is a Promise. This means that generic asynchronous data structures are unable to deal with Promises, and TypeScript sometimes fails to be typesafe:

1
2
3
4
5
async function identity<T>(t: T): Promise<T> {
    return t;
}

identity(Promise.resolve(5)).then((val: number) => alert(val))

Even if the val is a number because .then flattens the Promise, TypeScript does not recognize it as such:

1
2
3
Argument of type '(val: number) => void' is not assignable to parameter of type '(value: Promise<number>) => void | PromiseLike<void>'.
  Types of parameters 'val' and 'value' are incompatible.
    Type 'Promise<number>' is not assignable to type 'number'.

This design decision was likely made so we only need .then (takes object or Promise) and not .then (takes object) and .flatThen (takes Promise). Which is an understandable position for convenience, however there are better alternatives; if the callback of .then were to expect Promise<T>, but simply wrap any values that are not in a Promise into one, we wouldn’t get full type safety, but we’d get the convenience of a single .then method, but still allow us to create asynchronous data structure that may contain any objects, including Promises.

1
2
3
4
5
assert(await 5 === 5); // always succeeds
assert(await Promise.resolve(5) === 5); // always succeeds
assert(await Promise.resolve(Promise.resolve(5)) === 5); // succeeds with JavaScript's Promises, fails in ours
assert(await someObj === someObj); // fails in both definitions if someObj is a Promise; no full type safety
assert(await Promise.resolve(someObj) === someObj); // succeeds with our Promises, fails in JavaScript's if someObj is a Promise

With this alternative Promise definition, languages like TypeScript could get around type errors by simply disallowing (as a warning or compiler flag) awaiting anything that’s not a guaranteed Promise at compile-time.