1

There are many ways to make good control flows with ES2015 promises compared to alternatives like "callback hell" and similar. Lots of examples out there.

However, when the arguments for each step in the control flow depend on more than the return/resolved value of the previous step, it is harder to find a good syntax. Actual use cases are usually a bit more complex, so the simplified example syntaxes might not look that ugly, but it makes it easier to explain and discuss.

So, the question is how to make a simple (readable and easily maintainable) but flexible way of solving this kind of cases.

Example

Three operations in the control flow. Each being a function that returns a Promise.

function firstOperation(arg1) {
    return new Promise(...);
}

function secondOperation(firstResponse) {
    return new Promise(...);
}

function thirdOperation(firstResponse, secondResponse) {
    return new Promise(...);
}

Simpler control flows

If each step just depended on the previous one, it could look simething like this:

firstOperation('foo')
    .then(res1 => secondOperation(res1))
    .then(res2 => thirdOperation(res2));

Or even simpler:

firstOperation('foo')
    .then(secondOperation)
    .then(thirdOperation);

No problem there. But in this case, thirdOperation require arguments from both of the first two operations.

The future

In ES8, I guess this could look something like:

const res1 = await firstOperation('foo');
const res2 = await secondOperation(res1);
const res3 = await thirdOperation(res1, res2);

The present

I want to use the completed standards, so I hope to find the best possible solution for these kind of cases with ES2015 syntax or possibly a promise/generator library.

Possible (but not very simple/readable) ways to solve it:

firstOperation('foo')
    .then(res1 => secondOperation(res1)
        .then(res2 => thirdOperation(res1, res2))
    );

Making it a kind of "promise hell" that works, but in more complex cases (more than one level) would become ugly and hard to read/maintain.

Another solution:

firstOperation('foo')
    .then(res1 => secondOperation(res1)
        .then(res2 => ({ res1, res2 })))
    .then(({ res1, res2 }) => thirdOperation(res1, res2));

Not much prettier, the only benifit compared to the previous one, is that if there where more than three operations, they could all be called on the same level, instead of indenting one level further each time. And after each operation, the result is being merged with the other ones into a "context object" keeping all the results.

Suggestions?

So, until a standardized way comes along (ES8 probably), I guess using some kind of promise/generator library would be an acceptable solution. What I mostly hope, is that I won't need to touch the functions themselves. Like still letting them (firstOperation, secondOperation and thirdOperation in the examples) get arguments the normal way, and return a promise that resolves with the value directly, instead of having to rewrite them to be able to fit different control flow use cases.

Suggestions for solving cases like this?

henit
  • 1,150
  • 1
  • 12
  • 28
  • FYI async/await will be ES8, not ES7. – loganfsmyth Dec 07 '16 at 06:52
  • Does `res2` depend on `res1`? Or does only `res3` depend on `res1` and `res2`? – guest271314 Dec 07 '16 at 06:53
  • @loganfsmyth Thanks, will update the text. – henit Dec 07 '16 at 06:59
  • _"But in this case, thirdOperation require arguments from both of the first two operations."_ The example can return values from both `firstOperation('foo').then(secondOperation)` to `.then(thirdOperation)`; not sure what issue is? – guest271314 Dec 07 '16 at 06:59
  • @guest271314 The issue is that the call to `thirdOperation` require the resolved values from both of the other two operations (both async), so they can't simply be chained (like explained in the *Simpler control flows* example) – henit Dec 07 '16 at 07:03
  • @henit Yes, the values can be `return`ed from both `firstOperation` and `secondOperation` to `thirdOperation` – guest271314 Dec 07 '16 at 07:06
  • 1
    At present your first example under "The present" is the most common approach. –  Dec 07 '16 at 07:07
  • The closure solution(s) become a lot more readable if you do not indent `.then` (and possibly not even put a linebreak before it), but rather put a linebreak after the `=>` token as it is standard for multiline function bodies. – Bergi Dec 07 '16 at 07:42
  • @Bergi The initial problem is the same, but this question asks for a simple syntax (readability and maintainability) using ES2015, possibly in combination with some library. I therefore hope and believe there might be better solutions available out there, knowing that a lot change in the javascript world in two years. So I hope this question can be reopened. – henit Dec 07 '16 at 08:20
  • @henit No, nothing has changed. `async`/`await` is the universally accepted solution, so there are no new approaches developed. In ES6, you can use generators with a promise continuation library, and some of the other approaches benefit from ES6 syntactic sugar (arrow functions, parameter destructuring etc). If you are missing anything of feel an update is necessary, let me know. – Bergi Dec 07 '16 at 08:25
  • @Bergi I am now reading about `co` (had not seen it before). It seems like it can give me a `yield` syntax for the described cases, that would look similar to the future `await` syntax, just wrapped in a co-generator function. In a more complex flow, that is much more readable thanmost other alternatives, and very close to the future solution (like I described in the question), so less maintenance at the time when it can be replaced with the actual future solution. There might be other even better solutions, but there was no time for anyone to present it since you closed the question so fast. – henit Dec 07 '16 at 08:37
  • Ah, I thought you knew about `co` and co already as you wrote "*using some kind of promise/generator library would be an acceptable solution*". But really, you can believe me that there are no new solutions, and if there are I'd be happy to see a new answer on the canonical question. – Bergi Dec 07 '16 at 08:49
  • @Bergi Sure. I guess non-library solutions won't improve until `await` is part of the standard. I have been refactoring different ways as the flows and requirements changed the last couple of months, and I just wanted to end up with a solution that is both easy to read/maintain, and can let the functions remain decoupled and promise-based. The less rewrites needed when a better solution become part of the standard, the better. – henit Dec 07 '16 at 09:22

2 Answers2

2

You could do something like this:

const res1 = firstOperation('foo');
const res2 = res1.then(secondOperation);
const res3 = Promise.all([res1, res2])
    .then(args => thirdOperation(...args));
loganfsmyth
  • 156,129
  • 30
  • 331
  • 251
-1

I usually declare variables before the promise chain and store the results there, where all of the calls can access them. It works, doesn't require increased indenting / deeper leveling, and it doesn't require you to change your function params.

let firstResponse;

firstOperation(userId)
.then(firstResp=>{
    firstResponse = firstRestp;
    return secondOperation(firstResponse);
 })
.then(secondResponse=>{
    return thirdOperation(firstResponse, secondResponse);
});

Edit

This answer in the linked duplicate question points out some drawbacks to this approach. For example, the variables might be used before being populated.

Community
  • 1
  • 1
stone
  • 8,422
  • 5
  • 54
  • 66