For AI agents: the complete documentation index is available at /tc39-atlas/en/llms.txt, the full documentation bundle is available at /tc39-atlas/en/llms-full.txt, and this page is available as Markdown at /tc39-atlas/en/proposals/proposal-await-dictionary.md.
  • English
  • Await Dictionary S3

    Proposal details
    Proposal overview

    This proposal addresses the problem of efficiently awaiting multiple promises keyed by property names, avoiding sequential waterfalls and order-based confusion. It introduces Promise.allKeyed and Promise.allSettledKeyed static methods that take an object of promises and return a promise for an object with the same keys, resolving all in parallel.

    Note

    The README below comes from the upstream repository and may contain outdated stage or status metadata. Use the proposal details above as the current source of truth.

    Await dictionary of Promises

    Status

    Stage: 3

    Champions:

    Authors:

    Motivation

    await on individual properties creates a waterfall, rather than running requests in parallel:

    const obj = {
      shape: await getShape(),
      color: await getColor(),
      mass: await getMass(),
    };

    Promise.all helps, but is based on order, rather than names, which could lead to mixups:

    const [
      color,
      shape,
      mass,
    ] = await Promise.all([
      getShape(),
      getColor(),
      getMass(),
    ]);

    Solutions using existing syntax can be verbose and pollute the number of variables in scope:

    const shapeRequest = getShape();
    const colorRequest = getColor();
    const massRequest = getMass();
    
    const shape = await shapeRequest;
    const color = await colorRequest;
    const mass = await massRequest;

    Additionally the above pattern risks unhandled Promise rejections. If await shapeRequest throws, then no handler was attached to the colorRequest or massRequest promises - if either of these reject it will result in an unhandled Promise rejection error. On some systems this will cause the process to exit.

    Proposed Solution

    const {
      shape,
      color,
      mass,
    } = await Promise.allKeyed({
      shape: getShape(),
      color: getColor(),
      mass: getMass(),
    });

    This intentionally follows the shape of https://github.com/tc39/proposal-joint-iteration.

    As Promise.all is to Iterator.zip

    Iterator.zip = (Array<Iterator<T>>) => Iterator<Array<T>>
    Promise.all  = (Array<Promise<T>>)  => Promise<Array<T>>

    Promise.allKeyed is to Iterator.zipKeyed

    type Dict<V> = { [k: string | symbol]: V };
    
    Iterator.zipKeyed = <D extends Dict<Iterator<any>>>(iterables: D)
      => Iterator<{ [k in keyof D]: Nexted<D[k]> }>
    
    Promise.allKeyed  = <D extends Dict<Promise<any>>>(promises: D)
      => Promise <{ [k in keyof D]: Awaited<D[k]> }>

    Additional API

    This proposal also extends this feature to Promise.allSettled users.

    const results = await Promise.allSettledKeyed({
        shape: getShape(),
        color: getColor(),
        mass: getMass(),
    });
    if (results.shape.status === "fulfilled") {
      console.log(results.shape.value);
    } else {
      console.error(results.shape.reason)
    }

    Existing solutions

    LibraryOwnSymbols
    Bluebird.props
    combine-promises
    p-props

    Implementations

    Polyfill/transpiler implementations

    None.

    Native implementations

    None.

    Q&A

    Why not a deep-copy option?

    JSON.stringify aside, it is not common for builtin JavaScript APIs to traverse arbitrary objects deeply.

    Array.prototype.flat is deep, but only for the well defined boundaries of arrays.

    Why only own keys?

    This follows other builtins such as Object.keys and also matches https://github.com/tc39/proposal-joint-iteration.

    What about symbol keys?

    All enumerable properties are used, included enumerable symbols.

    This matches https://github.com/tc39/proposal-joint-iteration.

    This does differ from existing solutions, which follow Object.keys semantics (ignoring symbols). This difference is not perceived to be an issue due to the low usage of own enumerable symbols.

    Alternatives considered

    Promise.ownProperties

    const {
      shape,
      color,
      mass,
    } = await Promise.ownProperties({
      shape: getShape(),
      color: getColor(),
      mass: getMass(),
    });

    Promise.fromEntries

    const {
      shape,
      color,
      mass,
    } = await Promise.fromEntries(Object.entries({
      shape: getShape(),
      color: getColor(),
      mass: getMass(),
    }));

    Promise.all overload

    Dispatch depending if the argument is an iterable or not.

    const {
      shape,
      color,
      mass,
    } = await Promise.all({
      shape: getShape(),
      color: getColor(),
      mass: getMass(),
    });

    "This would avoid introducing a new name to the API surface. However, there is discomfort with the shape of the output depending on the shape of the input, and the risk of accidentally passing multiple arguments instead of an array. For example:

    Promise.all(p1, p2, p3); // ❌ should have been `Promise.all([p1, p2, p3])`

    While this currently throws as the p1 is not iterable, the overload would start to allow this call but not do what the caller intended.

    Dedicated syntax

    Inspired from other languages such as Swift - see: https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency#Calling-Asynchronous-Functions-in-Parallel

    async const shape = getShape();
    async const color = getColor();
    async const mass = getMass();
    
    const obj = await {
      shape,
      color,
      mass: Math.max(0, mass),
    };

    All references to an async const identifier within await <exp> are implicitly awaited.

    The above code would be (roughly) equivalent to:

    const $0 = getShape();
    const $1 = getColor();
    const $2 = getMass();
    
    const obj = await ((shape, color, mass) => ({
      shape,
      color,
      mass: Math.max(0, mass),
    }))(await $0, await $1, await $2);