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.

Sorry that I am reviving to this old topic, but it matches best what I desparately need to be added to JS. Because it will affect thousands if not more really awful constructs in my code which uses Promise.all and similar.

I always break my fingers by writing something like

const [ a,b,c,d,e ] = await Promise.all([ u,v,w,x,y ]);

Yes, this hurts me. Badly. As I need that nearly in 20% of my routines. Because concurrency is important!

What I'd rather like to see is

const await [ a,b,c,d,e ] = [ u,v,w,x,y ];

This looks very like for await. Adding it is possible as it currently is already detected as syntax error. So it creates no ambiguity.

Promise.all vs. Promise.allSettled

Note that I definitively do not agree to posts like "avoid Promise.all at all cost" due to people killing their own server by sending billions of fetch() in parallel, and hence recommend even more complex things like p-throttle. In contrast I always limit the number of concurrent fetch requests and live happily with Promise.all afterwards. (So I do something like const fetch20 = throttle(20, fetch); and then use fetch20 instead of fetch. With some suitable implementation of throttle. This has the added opportunity to have some fetch5 or fetch1 arround, too, which are independently queued parallel to the other fetches).

The question about above "const await", "let await" and (perhaps) "var await" construct is: Shall it use Promise.all or Promise.allSettled? I vote for the first, because it should NOT wait until all Promises settle but throw early. If this is a concern, use .catch() on the promises which shall not throw.

Also something like Promise.allSettled can be implemented with some syntax like Promise.all(promises.map(_ => _.catch(e => ({e}))), or, in my wanted case

const await [...a] = promises.map(_ => _.then(r => ({r}), e => ({e})));

You probably get the idea.

I think such code is much more readable than

const a = Promise.allSettled(promises);

Why? Because there I do not really see what is going on without a deep understanding of what Promise.allSettled() does. In contrast in the above way I know, that I get some Object {r} which contains the result and some object {e} which contains the error. No need to know how to do introspection on some obscure thing called Promise. Just some clear

if (a[0].e) // Oh there was some error on the first object

other discussed form of await in destructuring

I would vote against using await within the destructuring as shown in the other posts. Either all or nothing, because everything in between makes things unnecessary complex. And complexity shall be avoided at all cost.

Hence PLEASE DO NOT EVEN THINK ABOUT something like:

const [ await a, b ] = api();

or even more horrible

const { await a, b } = api();

Either

const [ _a, b ] = api();
const a = await _a;

or

const [ _a, b ] = api();
const await a = _a;

Why?

Because it is not really clear what the author wanted to say in something like

const [ await a, b, await c, d, await e ] = api();

Compare

(A)

const [ _a, b, _c, d, _e ] = api();
const await [ a, c, e ] = [ _a, _c, _e ];

vs. (which already works today):

(B)

const [ _a, b, _c, d, _e ] = api();
const a = await _a;
const c = await _c;
const e = await _e;

Which one was what the author preferred? You may argue "that makes no difference" but I definitively see some different behavior when it comes to error propagation.

It is right, that one probably expects that the error flow should perform as in "(A)". But then look at the construct itself again. await a, b, await c, d, await e is just uglyashell if you ask me .. and I alread hate to write things like this.

In contrast for await is already there. Why not accompany it with const await and let await?

And, btw, perhaps var await is bad, too, because of scoping rules. So if you ask me, where to add await, I'd vote for let and const but not var. var has it's use, but only if you really need to make forward declarations but want to keep the declaration at the definition (for DRY code). So in my code I do not need var often and do not see any added value in having var await, except, perhaps, for some symmetry.

So if adding let await and const await gives var await for free in the engines, Id say keep it. Else leave var` alone.

Perhaps even better optimization

const await { a, b. c } = something()

should be syntactic sugar for

const _a = something();
const [a,b,c] = await Promise.all([a,b,c]);

and not

const _a = await something();
const [a,b,c] = await Promise.all([a,b,c]);

of course. I only write this here because

const a = await api();

and

const await a = api();

happen to give the same result. But it might be that they are evaluated on some
different implementation path in the engines.

AFAICS const await can be far better optimized, because the destructuring is already known in advance (at least most times), while with Promise.all the destructuring can only happen after the Promises settle for real.

And finally

There shall be some difference between Promise.all() and destructuring:

With destructuring only the affected Promises (or Futures) are awaited.
While with Promise.all really all Promises are awaited.

Example:

const promises_array = [u,v,w,x,y,z];

// only u,v,w are awaited in parallel, x,y,z are left alone
const await [a,b,c] = promises_array;

// const await [a,b,c] = await Promise.all(promises_array);
// would await all promises in parallel unneccesarily
// so you must .slice() the array:
const await [a,b,c] = await Promise.all(promises_array.slice(0,3));
// which is highly error prone, consider adding one variable
//const await [a,b,c,d] = await Promise.all(promises_array.slice(0,3));
// OOPS forgot to increment the slice, too.

// And following (consider "await api()" is something more complex):
const await { a,b,c } = await api();

// needs to be expressed, today, with something like:
const [a,b,c] = await Promise.await((_=>[_.a,_.b._.c])(await api()));

At least that is what I see in my code. Far too often. Which makes it harder to understand than necessary, at least compared to the way with const await.

Have you seen https://github.com/tc39/proposal-await.ops ?