4

As far as I know, async/await is just syntactic sugar over promise.then. Consider this code snippet:

function sleep(n){
    return new Promise(res => setTimeout(res, n));
}

function* range(n){
    var i = 0;
    while(i < n)    yield i++;
}
async function doStuff(){
    for(let n of range(10)){
        console.log(n);          // print the number
        await sleep(1000);       // wait for 1 second
    }
}

async/await makes the code very linear, efficient and easy to understand. One thing to keep in mind is that range does not have to have an actual end for this to work.

The problem now is how this can be rewritten using pre-ES7 era's promise.then. Here's a possible implementation of the same loop:

function doStuff(){
    return Array.from(range(10)).reduce((acc, ele) => {
        return acc
            .then(() => console.log(ele))
            .then(() => sleep(1000))
    }, Promise.resolve());
}

Ignoring the fact that the code isn't quite elegant, the use of Array.from(range(10))

  1. creates an extra array that isn't needed, and
  2. assumes range(10) will end some point in the future.

Doesn't look like a good conversion.

We can also completely reinvent the wheel by using yield as await, but that would make the syntax non ES5-compliant. The goal here is to:

  1. Rewrite using ES5-compliant syntax
  2. Use the promise-returning sleep function
  3. Dynamically chain the sleep promise while allowing the iterator to not have an end
  4. doStuff can be chained:

    doStuff().finally(cleanUp);  // clean up if something failed
    
  5. (Optional) Code should not be overly complex

Any idea?

HMR
  • 37,593
  • 24
  • 91
  • 160
Derek 朕會功夫
  • 92,235
  • 44
  • 185
  • 247
  • @zerkms `range` can go as high as it wants and the `for` loop will still work, while `Array.from` will freeze. – Derek 朕會功夫 Dec 03 '17 at 23:48
  • 2
    have you tried using a transpiler to see what it would output? – Jaromanda X Dec 03 '17 at 23:48
  • What if you drop `Array.from` and use a loop? – zerkms Dec 03 '17 at 23:48
  • @JaromandaX Babel transpiles async functions into a generator and uses a custom-built generator runtime with over 100 lines of code which is not practical in day-to-day coding. I would love to know an easy ES5 conversion of the simple loop, if `async/await` really is merely syntactic sugar over `.then`. – Derek 朕會功夫 Dec 03 '17 at 23:52
  • It is, but in your code you substituted not only `async/await` but other stuff as well. – zerkms Dec 03 '17 at 23:54
  • well, your .reduce version looks very much like the way to do it ... but, `function*` isn't ES5 compatible anyway, so that would need to be rewritten too - `Array.from(range(10))` could be rewritten `Array.from({length:10}).map((_, v) => v)` – Jaromanda X Dec 03 '17 at 23:55
  • @JaromandaX Generators can be rewritten into ES5 style functions, but my main concern is whether `await`s in a `for` loop can be easily rewritten into ES5 style `promise.then`. – Derek 朕會功夫 Dec 03 '17 at 23:56
  • https://pastebin.com/aB85rcrg --- it can be easily rewritten. (and you already did it) – zerkms Dec 03 '17 at 23:57
  • in that case I can't see the "difference" in functionality between the async/await vs reduce code you've posted to be honest – Jaromanda X Dec 03 '17 at 23:57
  • @zerkms I don't think it works for `range(Infinity)`. – Derek 朕會功夫 Dec 03 '17 at 23:58
  • @Derek朕會功夫 define "works". It would generate infinite number of chained promises. – zerkms Dec 03 '17 at 23:58
  • 1
    ahhh, the crux of the matter is the size of the range :p I get you now – Jaromanda X Dec 03 '17 at 23:58
  • @zerkms Does not work as in the function will not terminate and return a proper promise, where `async/await` will return a promise, where if the body of the function throws an exception, the promise chain will continue as expected. – Derek 朕會功夫 Dec 03 '17 at 23:59
  • Then create an example that exactly reproduces/demonstrates your problem. At the moment the very code you provided can easily be converted. – zerkms Dec 04 '17 at 00:01
  • @zerkms The sample code in question already describes the problem in details. However, if you would like a complete sample in action, here's a fiddle: https://jsfiddle.net/DerekL/ac2eLcq5/ – Derek 朕會功夫 Dec 04 '17 at 00:02
  • 1
    @zerkms That's not an accurate translation though. The exception can be thrown in the body (ie the scope where `console.log` is) and there is no way you can know ahead of time while building the promise chain. – Derek 朕會功夫 Dec 04 '17 at 00:06
  • Yep. I see now .. – zerkms Dec 04 '17 at 00:06
  • @zerkms I guess we can always do [this](https://jsfiddle.net/DerekL/b901dnph/) but it looks like way too much code for such a simple loop. Also I'm not sure if that will cause a stack overflow. – Derek 朕會功夫 Dec 04 '17 at 00:23
  • Btw, was it triggered by http://2ality.com/2017/12/for-await-of-sync-iterables.html somehow or just is a coincidence? – zerkms Dec 04 '17 at 00:28
  • @zerkms Seems like a great blog site but it was just a coincidence I suppose. – Derek 朕會功夫 Dec 04 '17 at 00:40
  • It's ES2017. Not ES7. If you want to make it work close original (including generators), you will end up with a big pile of code. It makes sense to use TS or Babel at this point, because this is what they are for (TS output for generators and async/await is generally cleaner). It's unclear what problems you have with 'The goal here is to...'. What did you try? – Estus Flask Dec 04 '17 at 01:42
  • @estus "*What did you try*" - If you look closely, you can see that there is a sample implementation that does not accurately reflect original code. Furthermore, ES7 is just a name that many will understand. You might as well start telling browser vendors to stop using the term "JavaScript" because it's technically ECMAScript. Regarding TypeScript and Babel, it's often not practical to actually use them in production because they tend to generate a lot more code when there is no equivalent syntax in ES5. Even the content is gzipped, the generated code adds extra overheads that can be avoided. – Derek 朕會功夫 Dec 04 '17 at 01:49
  • ES7 is ambiguous name that was coined before ES6/ES2015 release. There's no such thing as ES7 because specs were named as 20* since then. async/await belongs to ES2017. I see the implementation. But I don't see what you've tried with *The goal here is to* list. If you have specific problems with each of listed items, please, explain them. Otherwise it looks like a request for code, which is not a question and thus off-topic. – Estus Flask Dec 04 '17 at 02:25
  • You will have to write a fair amount of code to fully implement a generator in ES5 if you need that, that's the point. While TS output will likely be 1-2kb gzipped, and it won't add up for multiple async/await and generator functions. Babel is more verbose. – Estus Flask Dec 04 '17 at 02:32
  • @estus Once again, the sample code attempts to achieve the first 4 goals, with the only problem of it not being an accurate translation when it comes to handling promises in a `for` block. – Derek 朕會功夫 Dec 04 '17 at 03:05
  • @estus You will have to provide a few valid arguments if you wish to continue considering this question as *off-topic*. Objectively, this question contains a code snippet that is explicitly stated to achieve the list goals but failed, and the reason it failed is clearly explained. Honestly, I am not able to see exactly why you would considered it as a malformed question. Perhaps you had a bad day, but none of this discussion contributes to solving the actual problem. – Derek 朕會功夫 Dec 04 '17 at 03:06
  • The actual problem is that there's no clear problem statement, 'Doesn't look like a good conversion' isn't the one. You didn't show the efforts to solve any of these 5 'goal' items on your own, so it's unclear what problems you met (#2 and #3 are already there). I guess that somebody with your rep was on SO long enough to know that 'write the code for me' questions that lack the research aren't well-received and end up negatively voted or closed as 'too broad'. I didn't vote for the question in its current state, so it's strange to see that this one still wasn't. Btw, my day is fine, thanks. – Estus Flask Dec 04 '17 at 06:23
  • @estus Somebody with your rep should know that this question is a good fit for Stack Overflow. Try reading the support page to get a grasp of what a good question is. Nonetheless, my advice is to go on to Stack Overflow meta if you like daily debates with strangers online; frankly I'm here to look for constructive discussions instead of pointless arguments. – Derek 朕會功夫 Dec 04 '17 at 18:09
  • I'm not interested in arguing about nothing. My point was to improve the question to the point where it could conform to SO guidelines and be constructively answered. Asking users to write a considerable chunk of code that you could potentially write yourself is NOT a good question (not a question). I see lazy 'write the code for me' questions being closed on SO all the time. Since you convinced the guys to do that for you any way... well, good for you. – Estus Flask Dec 04 '17 at 18:34

2 Answers2

1

I think the following may do the trick, your example doesn't show what to do with resolve value and how it relates to the iterator values so I made a change to how sleep is called.

Some promise polyfils may run out of stack space with high promise chains so you should check your polyfil (if its implementation returns and continues with a setTimeout the stack should clear but some polyfils may not implement it this way).

    function sleep(n){
      return new Promise(res => setTimeout(_=>res(n/100), n));
    }

    function* range(n){
      var i = 0;
      while(i < n)    yield i++;
    }

    function doStuff(){
      const processValue = 
        resolve => {
          console.log("resolved with:",resolve);
          // if(resolve===3){throw "nope";}
          return sleep(resolve*100);
        },
      rec = (p,iter) => {
        const result = iter.next();
        if (result.done){
          return p;
        }
        p = p.then(_=>processValue(result.value))
        return p.then(
          resolve=>{
            return rec(p,iter)
          }
        );
      },
      iter = range(10),
      firstResult = iter.next();
      if(firstResult.done){
        return processValue(firstResult.value);
      }
      return rec(processValue(firstResult.value),iter);
    }

    doStuff()
    .then(
      x=>console.log("done:",x)
      ,reject=>console.warn("fail:",reject)
    );
HMR
  • 37,593
  • 24
  • 91
  • 160
1

I've always said that if you need an asynchronous design pattern first look at the async library. In this case, since you're using promises, take a look at the promisified async-q library. The translation is straight forward:

var n = 0;
async.whilst(() => n < 10, () => {
    n++;
    console.log(n);
    return sleep(1000);
})
slebetman
  • 109,858
  • 19
  • 140
  • 171