JS: how to use generator and yield in a callback
Asked Answered
M

4

28

I use JS generator to yield a value in a callback of setTimeout:

function* sleep() {
  // Using yield here is OK
  // yield 5; 
  setTimeout(function() {
    // Using yield here will throw error
    yield 5;
  }, 5000);
}

// sync
const sleepTime = sleep().next()

Why I can't yield values inside a callback in the generator?

Marquesan answered 26/12, 2016 at 3:41 Comment(0)
D
15

function* declaration is synchronous. You can yield a new Promise object, chain .then() to .next().value to retrieve resolved Promise value

function* sleep() {
  yield new Promise(resolve => {
    setTimeout(() => {
      resolve(5);
    }, 5000);
  })
}

// sync
const sleepTime = sleep().next().value
  .then(n => console.log(n))
  .catch(e => console.error(e));
Disclimax answered 26/12, 2016 at 3:54 Comment(6)
This only resolves once, so it doesn't generate much :) How would the code repeatedly yield from a setInterval callback, for example?Aurelio
.then(n => console(n)) should be .then(n => console.log(n))Mobile
what's the point of a generator that only yields once?Guardado
This worked for me. I needed a generator to yield once for a spawn process which executes an ADB exec-out. I catch the stdout from the pipe just AFTER the process finishes executing. I then yield the resolve promise with the string. Works like a charm for this type of usage :)Quarter
> what's the point of a generator that only yields once? -> Browser compatibilityAmah
This answer is useless. The whole point of generators is to yield multiple times, otherwise it doesn't have to be a generator.Limbert
G
9

I came to this question looking for a way to convert a callback that is called every so often (e.g. a Node stream, event listener, or setInterval callback) into an async iterable, and after some research I found this NPM package that does all that: EventIterator.

EventIterator is a small module that greatly simplifies converting event emitters, event targets, and similar objects into EcmaScript async iterators. It works in browser and Node.js environments.

A basic setInterval iterable:

import { EventIterator } from "event-iterator"
const counter = ms =>
  new EventIterator(({ push }) => {
    let count = 0
    const interval = setInterval(() => push(++count), ms)
    return () => clearInterval(interval)
  })

for await (const count of counter(1000)) console.log(count)

(think of push like yield).

Though this doesn't technically answer the question, the accepted answer doesn't really answer it either, and this solution seems pretty close to what the OP was looking for.

Grease answered 7/8, 2022 at 16:6 Comment(2)
This should be the accepted answer. The other one is utterly useless, since it can only yield once. OTOH, I could successfully use EventIterator to create an iterator easily.Rider
Don't forget to mention the hazards of this approach: "If you cannot reasonably consume all emitted events with your async iterator; the internal EventIterator queue can fill up indefinitely."Waynant
C
2

To directly answer the question, it is not possible to do this using the "*" / "yield" syntax. From here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/yield

...the "yield" keyword may only be used directly within the generator function that contains it. "It cannot be used within nested functions" such as callbacks.

The unhelpful answer to the OP's question of "why" is that it is prohibited by the ECMAScript language specification, at least in strict mode: https://262.ecma-international.org/9.0/#sec-generator-abstract-operations

For more of an intuition of why: the implementation of a generator is that the "yield" keyword pauses execution of its generator function. The execution of the generator function is otherwise ordinary, and when it returns, the iterable that it generated ends. That signals to the caller that no more values are coming, and any loop waiting for it will also end. After that, there's no opportunity to yield anything to any interested caller, even if the nested callback runs again.

Although a callback or other nested function can bind variables from the outer generator, it could escape the generator's lifetime and be run any other time / place / context. This means the desired yield keyword may have no function to pause, and no caller or loop to yield a value to. I would speculate that the strict mode syntax error was put here to save code authors from having something silently undesirable happen.

That said, the generator syntax is not necessary to create the OP's desired effect. When the goal is just to get "next()" to work, or to participate in the async iterator protocol ("for await (...)"), those protocols can be conformed to using ordinary functions and objects, without need for yield. The protocol you want to conform to is documented here:

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols

...the other answer mentioning EventIterator is an example of helper code that makes this easier to do.

Christinachristine answered 5/3, 2023 at 17:52 Comment(1)
Adding a link to exploringjs.com/es6/… as another source for why it's not allowedZarger
P
0

Extending the guest271314's answer.

The below code yields in a loop. Thus can yield more than once.

async function* foo(loopVal) {
  for(let i=0;i<loopVal;i++){
    yield new Promise(resolve => {
      setTimeout(() => {
        resolve(i);
      }, 5000);
    })
  }
}

(async function() {
  for await (const num of foo(5)) {
    console.log(num);
  }
})();
Painkiller answered 5/1, 2023 at 6:26 Comment(1)
This answer isn't right since it just yields five timeouts immediatelyGrease

© 2022 - 2025 — McMap. All rights reserved.