await keyword for destructuring assignment

When a promise is deeply embedded in another object, such as:

const promise = new Promise(r => r({ foo: 'bar' }));
const arr = [ promise ];
const obj = { promise };

Currently we can get the value like this.

const { foo } = await arr[0];
const { foo } = await obj.promise;

It would be nice to be able to take it out like this.

const [ await { foo } ] = arr;
const { promise: await { foo } } = obj;

And it would be good if the same applies even if it is not wrapped in another object.

const await { foo } = promise;

Could you perhaps help us to picture the value of this by providing us with a concrete scenario you've run into where this would be valuable?

I've seen promises get stored in objects, but in general, that seems to be much more rare than common - at least from my experience.

1 Like

For const arr = [ promise ];, you'd be able to do const [foo] = await Promise.all(arr) (with perhaps arr.slice(0) if you didn't want to await other promises in the array).

3 Likes

What I want is to get a value at once for a complex object with promises mixed in a field, without having to write the variable declaration multiple times.

const api = () => [promiseResult, nonPromiseMetadata];

// before
const [promiseResult, metadata] = api();
const result = await promiseResult;

// after
const [await result, metadata] = api();

For an array this wouldn’t work, but for an object this is a bit hacky but should work:

const { x: xPromise, doesNotExist: x = await xPromise } = obj;

Come on! Guy asks for syntax sugar, you offer code obfuscation and a footgun :D

For the record, from the OP examples, I find the current code easier to follow; don't see a need for a different, more verbose and (imo) obscure way of writing that.

1 Like

As was mentioned, for that you can use

const [result, metadata] = Promise.all(api());

although it's a bit weird that the api returns a structure containing promises and not a promise for the structure. It seems like they'd expect you to do something with the metadata before waiting the promise for the result, but that's a well-known antipattern as it can easily result in unhandled rejections.

@bergus - forgot the await :)

I wouldn't go as far as to call that an antipattern. Perhaps this is a promise that's already being awaited elsewhere or something. Dunno

@disjukr - could you perhaps enlighten us with a little more context? This example is still pretty abstract - it's hard to tell why you needed a promise with metadata.

For example: The closest concrete example I can think of is this: On the API layer, I needed to make requests to gather user information to see if a particular user was allowed to be making the request they were making. I wanted to memoize the results, so I stored these promises in a user-id-to-promised-result map, but also returned the promises. A further request would simply receive the memoized promise.

This example, unfortunately, doesn't benefit from destructuring, as I'm only ever pulling out one specific promise from the map. There's no way to destructure a map, nor would it be that useful to destructor just one thing.

But that's the closets concrete example I've got.

Oh yes, I'm sorry, and thanks for asking kindly and carefully. I'm not comfortable with english, so I'm a little tired of writing a long one.

As shown in the example I wrote right above, being able to use the await keyword in destructuring assignments can sometimes reduce the need to split variable declarations.

I'm not saying just don't await on the right side of the declaration, but in terms of langauge consistency, there is no reason not to support destructuring assignments of promises, which are primitive objects with dedicated syntax(async/await), while supporting that only for objects and arrays.

Of course, we should avoid making the language too complicated, but this doesn't even introduce new keyword or symbol and can be thought of intuitively, so isn't consistency more important?

Here is an actual use case. I'm currently writing a code generator for protobuf/gRPC, and I'd like to provide an option to generate code with access to metadata in some cases. (In contrast to the example I wrote above, here metadata is a promise. The response is either promise or async generator)

I wanted to give the user an example where they can access both the response and metadata with as little code as possible for whom using this interface.

If they wants to process a non-promise side asap, it has to be processed separately first before awaiting for the promise side, but probably not always they wants to, and I want to leave it up to the user to decide whether to solve it complicatedly or simply.

And for solve simply, I thought it should be possible to destructuring assignment of promises.

Thanks for the example :)

Of course, we should avoid making the language too complicated, but this doesn't even introduce new keyword or symbol and can be thought of intuitively, so isn't consistency more important?

I will argue that adding support for "await" when destructuring does not improve consistency.

await is a unary operator, that takes a value on the right-hand-side and "waits" for it.

No other unary operators are allowed to be used in destructuring syntax, even though it's theoretically ​possible to add support for them:

const [yield x] = array
// same as `const x = yield array[0]`

const [!x] = array
// same as `const x = !array[0]`

// etc...

However, if we think await-while-destructuring is useful enough, perhaps an exception could be made for await, that'll make the language a little less consistent. Hence, it's important to know how much of a real-world issue this is.

2 Likes

Oh, I must have been too short-sighted. I never thought of the cases you wrote.

Regarding an error can occur during await, I want to point out that it can also occur while destructuring an object field or an array item. Of course, await is mostly used for IO, so errors will occur much more often...

const { foo } = { get foo() { throw Error } }; // error
const arrlike = { [Symbol.iterator]: () => arrlike, next() { throw new Error } };
const [ foo ] = arrlike; // error

I agree that we need more real world examples to apply to language.