Proposal: then operator for easier promise manipulation

# Bryant Petersen (4 months ago)

I've put the details here: bwpetersen/proposal-then-operator

The basic idea is that the then operator would allow you to manipulate a promise as if it were its resolved value. Expressions containing then become similar to an onFulfillment handler.

Here is an example:

const greeting = `Hello, ${(then getUser()).name}. You have ${(then
getMessages()).length} messages.`
// This would behave like:
const greeting = async () => {
  const user = getUser()
  const messages = getMessages()
  return `Hello, ${(await user).name}. You have ${(await
messages).length} messages.`
}()

I'm looking for feedback on this idea.

# Peter Jaszkowiak (4 months ago)

Why not just use async/await then? Seems like if you were to replace then with await in your top example it would work exactly as you want (as long as it's in an async function).

# Bryant Petersen (4 months ago)

The purpose of await is to allow synchronous execution within an async function.

The purpose of then is to make it easier to manipulate the promises as if they were normal values. It is much closer to Promise.prototype.then than to await, since it does not prevent the statements following it from execution. For this reason it doesn't need to be in an async function.

The synchronous nature of await can be annoying when you have multiple promise-creating expressions that you want to run in parallel. You have to run the expressions first, keep track of the promises they evaluate to, and then use await on those promises. Promise.all helps in this endeavor, but it prevents you from putting expressions exactly where you want them to be.

Consider the following:

const pets = (await getDogs()).concat(await getCats())

This would not be ideal if you want getDogs and getCats to start off in parallel. So you need to do something like this:

const [dogs, cats] = await Promise.all([getDogs(), getCats()])
const pets = dogs.concat(cats)

With then it could be done like so:

const pets = await (then getDogs()).concat(then getCats())
# Tab Atkins Jr. (4 months ago)

On Sat, Mar 3, 2018 at 1:04 AM, Bryant Petersen <petersen.ok at gmail.com> wrote:

The purpose of await is to allow synchronous execution within an async function.

The purpose of then is to make it easier to manipulate the promises as if they were normal values. It is much closer to Promise.prototype.then than to await, since it does not prevent the statements following it from execution. For this reason it doesn't need to be in an async function.

The synchronous nature of await can be annoying when you have multiple promise-creating expressions that you want to run in parallel. You have to run the expressions first, keep track of the promises they evaluate to, and then use await on those promises. Promise.all helps in this endeavor, but it prevents you from putting expressions exactly where you want them to be.

Consider the following:

const pets = (await getDogs()).concat(await getCats())

This would not be ideal if you want getDogs and getCats to start off in parallel. So you need to do something like this:

const [dogs, cats] = await Promise.all([getDogs(), getCats()])
const pets = dogs.concat(cats)

With then it could be done like so:

const pets = await (then getDogs()).concat(then getCats())

I'm not sure how you're able to continue running code after the first "then". The .concat part is a property lookup on the first value, which can run arbitrary code - what object does that code operate on? How do you get to the point of even noticing that there's an argument to the function that also needs to be then'd?

I think the only way to get this to work would be to do a pre-parse over the function, looking for "then" expressions, and collect them all together into an implicit "await Promise.all(...)", effectively run at the beginning of the function when you execute it "for real". (Kinda similar to var hoisting, which implicitly puts a lot of var x = undefined; statements at the top of your function.) How this squares away with the possibility of a later "then" actually depending on the value of an earlier "then" expression, or on a value outside of the "then" expressions, is unclear.

Using Promise.all() to avoid synchronously waiting on each promise one-by-one is a little awkward sometimes, but it gives you obvious, well-defined semantics, which this doesn't appear to have.

# Bryant Petersen (4 months ago)

An expression which contains a then operator is not run synchronously. It does not run until its then expressions are resolved. It is like the body of a callback function, since the statements following it will most likely run before it does.

The way I would implement this is by having certain expressions search their subexpressions (i.e. AST decendants) for then expressions. If it finds one, it converts itself to an immediately invoked async function expression. This IIAFE's body will run the arguments to each then expression (so they're run in parallel), after that it will return an expression very similar to its original form. The IIAFE evaluates to a promise resolving to what you'd expect the original expression to resolve to.

The only thing at this point I consider not to be well-defined is what "certain expressions" need to do this search and conversion. I call this out in my more detailed explanation of the proposal: bwpetersen/proposal-then-operator#cutoff-points. I think there are obvious heuristics for deciding which expressions should do this.

# Peter Jaszkowiak (4 months ago)

This operator doesn't make any sense to me. It has to know not only the immediate expression to its left and/or right like normal operators.

It's absolutely not a unary operator, as it has to have information about every outer operator. It's almost an inverted operator in that sense.

If we're saying that the precedence is equal to the await operator, then what does await then x result in? Given your examples, it should result in something like x.then(x => await x)

Heck, how do you operate on the resulting promise without an intermediate variable? Can you even?

This is extremely unintuitive, as (then x) should behave the same regardless of where it appears. With your proposal, it's completely inconsistent.

const y = something(then x);
y.then(x => log(x));

// is equivalent to
x.then(x => something(x)).then(x=> log(x))

// vs

(something(then x)).then(x => log(x));

// is equivalent to
x.then(x => something(x).then(x => log(x)));

It is crazy that this kind of ambiguity could even be considered. How do you determine how far up in the expression tree you go?

Plus, the usefulness of this is marginal at best since we already have async/await.

On Mar 3, 2018 21:00, "Bryant Petersen" <petersen.ok at gmail.com> wrote:

An expression which contains a then operator is not run synchronously. It does not run until its then expressions are resolved. It is like the body of a callback function, since the statements following it will most likely run before it does.

The way I would implement this is by having certain expressions search their subexpressions (i.e. AST decendants) for then expressions. If it finds one, it converts itself to an immediately invoked async function expression. This IIAFE's body will run the arguments to each then expression (so they're run in parallel), after that it will return an expression very similar to its original form. The IIAFE evaluates to a promise resolving to what you'd expect the original expression to resolve to.

The only thing at this point I consider not to be well-defined is what "certain expressions" need to do this search and conversion. I call this out in my more detailed explanation of the proposal: bwpetersen/proposal-then-operator#cutoff-points. I think there are obvious heuristics for deciding which expressions should do this.

# Bryant Petersen (4 months ago)

Thank you for the questions.

How do you determine how far up in the expression tree you go?

I think this is a good question. When I go through various expressions, it seems obvious to me given the intent of the operator where to draw the line. In cases like let x = ..., function() { ... }, await ..., and then ... it shouldn't go higher than .... Part of the motive for posting it here is to get some discussion about this.

If we're saying that the precedence is equal to the await operator, then what does await then x result in? Given your examples, it should result in something like x.then(x => await x)

It shouldn't go higher than the ... in await ..., so it results in something like await x.then(x => x).

Heck, how do you operate on the resulting promise without an intermediate variable? Can you even?

You get to operate on the promise as if it were any value. Would you be specific about the operation you would like to do that you think becomes inaccessible? My best guess is you would want to use Promise.resolve.