jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

JavaScript Generators & yield — A Practical Guide with 10 Use Cases

A bilingual deep-dive into generator functions and yield: the pausable-function mental model, core syntax, two-way communication, async generators, plus 10 real-world use cases and practice exercises.

The Mental Model — A Pausable Function {Mô hình tư duy — Hàm có thể tạm dừng}

A normal function runs to completion {Hàm thường chạy đến khi xong}: you call it, it runs, it returns once {bạn gọi, nó chạy, nó return một lần}. A generator function is different {Generator function thì khác} — it can pause in the middle, hand a value back, and resume later {nó có thể tạm dừng giữa chừng, trả về một giá trị, rồi tiếp tục sau}.

Think of it like a video you can pause {Hãy nghĩ nó như video bạn có thể tạm dừng}: yield is the pause button {yield là nút pause}, and .next() is the play button {và .next() là nút play}.

function* counter() {
  console.log("start");
  yield 1; // pause here, give back 1 {dừng ở đây, trả về 1}
  console.log("resumed");
  yield 2; // pause again, give back 2 {dừng tiếp, trả về 2}
  console.log("done");
}

const gen = counter();      // nothing runs yet {chưa chạy gì cả}
gen.next(); // logs "start",    returns { value: 1, done: false }
gen.next(); // logs "resumed",  returns { value: 2, done: false }
gen.next(); // logs "done",     returns { value: undefined, done: true }

Two things make generators special {Hai điều khiến generator đặc biệt}:

  1. Lazy execution {Thực thi lười}: code doesn’t run until you ask for the next value {code không chạy đến khi bạn yêu cầu giá trị tiếp theo}.
  2. State is preserved {Trạng thái được giữ}: local variables survive between pauses {biến cục bộ tồn tại giữa các lần dừng}.

Core Syntax {Cú pháp cơ bản}

Declaring & Calling {Khai báo & Gọi}

// The asterisk * makes it a generator {Dấu * biến nó thành generator}
function* myGen() {
  yield "a";
  yield "b";
}

// Calling returns an iterator — it does NOT execute the body
// {Gọi trả về một iterator — KHÔNG thực thi thân hàm}
const it = myGen();

.next() and the Result Object {.next() và đối tượng kết quả}

Every .next() call returns an object {Mỗi lần gọi .next() trả về một object}:

const it = myGen();
it.next(); // { value: "a", done: false }
it.next(); // { value: "b", done: false }
it.next(); // { value: undefined, done: true } {hết rồi}
  • value — what yield gave back {thứ mà yield trả về}
  • donefalse while paused, true when finished {false khi đang dừng, true khi xong}

Consuming with for...of and Spread {Tiêu thụ bằng for...of và spread}

You rarely call .next() by hand {Bạn hiếm khi gọi .next() thủ công}. Generators are iterable {Generator là iterable}, so use the tools you already know {nên dùng các công cụ bạn đã biết}:

function* fruits() {
  yield "apple";
  yield "banana";
  yield "cherry";
}

// for...of automatically calls .next() until done
// {for...of tự động gọi .next() đến khi done}
for (const f of fruits()) {
  console.log(f); // apple, banana, cherry
}

// Spread collects all yielded values {Spread gom mọi giá trị yield}
const arr = [...fruits()]; // ["apple", "banana", "cherry"]

// Destructuring works too {Destructuring cũng được}
const [first, second] = fruits(); // first="apple", second="banana"

Note {Lưu ý}: for...of and spread ignore the final return value {for...of và spread bỏ qua giá trị return cuối} and stop at done: true {và dừng ở done: true}.


Advanced Mechanics {Cơ chế nâng cao}

Two-Way Communication — Passing Values Into next() {Giao tiếp hai chiều — truyền giá trị vào next()}

This is the most misunderstood feature {Đây là tính năng bị hiểu lầm nhiều nhất}. yield is not just an output {yield không chỉ là đầu ra} — it’s also an input {nó còn là đầu vào}. Whatever you pass to .next(x) becomes the result of the yield expression that was paused {Bất cứ gì bạn truyền vào .next(x) trở thành kết quả của biểu thức yield đang bị dừng}:

function* conversation() {
  const name = yield "What's your name?";
  const age = yield `Hello ${name}, how old are you?`;
  return `${name} is ${age} years old`;
}

const chat = conversation();
chat.next();        // { value: "What's your name?", done: false }
chat.next("Vinix"); // "Vinix" becomes the value of the first yield
                    // { value: "Hello Vinix, how old are you?", done: false }
chat.next(28);      // 28 becomes the value of the second yield
                    // { value: "Vinix is 28 years old", done: true }

Key point {Điểm mấu chốt}: the first .next() argument is always ignored {tham số của lần .next() đầu tiên luôn bị bỏ qua} because there’s no paused yield waiting for it yet {vì chưa có yield nào đang dừng để nhận nó}.

yield* — Delegating to Another Iterable {yield* — Uỷ quyền cho iterable khác}

yield* delegates iteration to another generator or iterable {yield* uỷ quyền việc lặp cho generator/iterable khác}:

function* inner() {
  yield 2;
  yield 3;
}

function* outer() {
  yield 1;
  yield* inner(); // delegate — yields 2, then 3 {uỷ quyền — yield 2, rồi 3}
  yield 4;
}

console.log([...outer()]); // [1, 2, 3, 4]

// Works with any iterable {Hoạt động với mọi iterable}
function* spreadString() {
  yield* "hi";       // yields "h", "i"
  yield* [10, 20];   // yields 10, 20
}
console.log([...spreadString()]); // ["h", "i", 10, 20]

This is the foundation for recursive traversal {Đây là nền tảng cho duyệt đệ quy} (see the tree example below) {(xem ví dụ cây bên dưới)}.

return and .throw() {return.throw()}

function* withReturn() {
  yield 1;
  return 99; // sets done:true with value 99 {đặt done:true với value 99}
  yield 2;   // never reached {không bao giờ tới}
}

const g = withReturn();
g.next(); // { value: 1, done: false }
g.next(); // { value: 99, done: true }
g.next(); // { value: undefined, done: true }

// .throw() injects an error at the paused yield {.throw() ném lỗi tại yield đang dừng}
function* safe() {
  try {
    yield "working";
  } catch (e) {
    console.log("caught:", e); // caught: oops
    yield "recovered";
  }
}
const s = safe();
s.next();              // { value: "working", done: false }
s.throw("oops");       // logs "caught: oops", { value: "recovered", done: false }

Async Generators — for await...of {Async Generator — for await...of}

Combine async + function* to yield promises over time {Kết hợp async + function* để yield promise theo thời gian}:

async function* fetchPages() {
  let url = "/api/items?page=1";
  while (url) {
    const res = await fetch(url);
    const data = await res.json();
    yield data.items;      // yield each page's items {yield items mỗi trang}
    url = data.nextPage;   // null when no more pages {null khi hết trang}
  }
}

// Consume with for await...of {Tiêu thụ bằng for await...of}
for await (const items of fetchPages()) {
  renderItems(items);
}

Live Demo {Demo trực tiếp}

Theory clicks faster when you can see it {Lý thuyết “thông” nhanh hơn khi bạn nhìn thấy nó}. This demo lets you {Demo này cho bạn}: step a generator with .next() {bước generator bằng .next()} and watch it pause at each yield {và xem nó dừng ở mỗi yield}, trace a lazy filter → map → take pipeline value-by-value {theo dõi pipeline lười filter → map → take từng giá trị}, and race a blocking loop against cooperative scheduling {và cho vòng lặp blocking đua với cooperative scheduling} to see how a generator keeps the UI responsive {để thấy generator giữ UI mượt thế nào}.

Open the full demo {Mở demo đầy đủ}: /tools/generators-demo/.


10 Practical Use Cases {10 Use Case thực tế}

1. Infinite Sequences & Unique IDs {Chuỗi vô hạn & ID duy nhất}

A generator can be infinite because it only computes on demand {Generator có thể vô hạn vì nó chỉ tính khi cần}:

function* idGenerator(prefix = "id") {
  let n = 1;
  while (true) {
    yield `${prefix}_${n++}`;
  }
}

const ids = idGenerator("user");
ids.next().value; // "user_1"
ids.next().value; // "user_2"
ids.next().value; // "user_3"

Use case {Use case}: stable unique keys for dynamically created elements {key duy nhất ổn định cho element tạo động}, without a global counter variable leaking everywhere {mà không cần biến đếm toàn cục rò rỉ khắp nơi}.

2. Lazy Ranges {Range lười}

Python has range(); JavaScript doesn’t {Python có range(); JavaScript thì không} — but a generator gives you one {nhưng generator cho bạn một cái}:

function* range(start, end, step = 1) {
  for (let i = start; i < end; i += step) {
    yield i;
  }
}

console.log([...range(0, 5)]);       // [0, 1, 2, 3, 4]
console.log([...range(0, 10, 2)]);   // [0, 2, 4, 6, 8]

for (const i of range(1, 4)) {
  console.log(i); // 1, 2, 3
}

Unlike building an array, range(0, 1_000_000) uses almost no memory {Khác với tạo mảng, range(0, 1_000_000) dùng gần như không tốn bộ nhớ} until you actually iterate {cho đến khi bạn thực sự lặp}.

3. Custom Iterables with Symbol.iterator {Iterable tuỳ chỉnh với Symbol.iterator}

Make your own class work with for...of and spread {Cho class của bạn hoạt động với for...of và spread}:

class LinkedList {
  constructor() {
    this.head = null;
  }
  add(value) {
    this.head = { value, next: this.head };
    return this;
  }
  // A generator method IS the iterator {Một method generator CHÍNH LÀ iterator}
  *[Symbol.iterator]() {
    let node = this.head;
    while (node) {
      yield node.value;
      node = node.next;
    }
  }
}

const list = new LinkedList().add(1).add(2).add(3);
console.log([...list]); // [3, 2, 1]
for (const v of list) console.log(v); // 3, 2, 1

4. Lazy Pipeline — map / filter / take {Pipeline lười — map / filter / take}

Process infinite or huge streams without intermediate arrays {Xử lý stream vô hạn hoặc khổng lồ mà không tạo mảng trung gian}:

function* map(iterable, fn) {
  for (const x of iterable) yield fn(x);
}

function* filter(iterable, predicate) {
  for (const x of iterable) if (predicate(x)) yield x;
}

function* take(iterable, n) {
  let count = 0;
  for (const x of iterable) {
    if (count++ >= n) return;
    yield x;
  }
}

// Compose lazily over an INFINITE sequence {Kết hợp lười trên chuỗi VÔ HẠN}
function* naturals() {
  let n = 1;
  while (true) yield n++;
}

const result = [
  ...take(
    map(
      filter(naturals(), (x) => x % 2 === 0), // even numbers {số chẵn}
      (x) => x * x                            // square them {bình phương}
    ),
    5 // only first 5 {chỉ 5 cái đầu}
  ),
];
console.log(result); // [4, 16, 36, 64, 100]

Each value flows through the whole pipeline one at a time {Mỗi giá trị chảy qua toàn bộ pipeline từng cái một} — no array of all even numbers is ever built {không bao giờ tạo mảng tất cả số chẵn}.

5. Tree / Graph Traversal with yield* {Duyệt cây / graph với yield*}

Recursive yield* makes depth-first traversal trivial {yield* đệ quy khiến duyệt theo chiều sâu cực dễ}:

const tree = {
  value: 1,
  children: [
    { value: 2, children: [{ value: 4, children: [] }] },
    { value: 3, children: [] },
  ],
};

function* walk(node) {
  yield node.value;
  for (const child of node.children) {
    yield* walk(child); // delegate to the recursive call {uỷ quyền cho lời gọi đệ quy}
  }
}

console.log([...walk(tree)]); // [1, 2, 4, 3]

// Find the first matching node lazily — stops as soon as found
// {Tìm node khớp đầu tiên một cách lười — dừng ngay khi tìm thấy}
function findFirst(node, predicate) {
  for (const value of walk(node)) {
    if (predicate(value)) return value;
  }
}
console.log(findFirst(tree, (v) => v > 2)); // 4

6. State Machines {Máy trạng thái}

A generator’s preserved state is perfect for modeling steps {Trạng thái được giữ của generator hoàn hảo để mô hình hoá các bước}:

function* trafficLight() {
  while (true) {
    yield "green";
    yield "yellow";
    yield "red";
  }
}

const light = trafficLight();
light.next().value; // "green"
light.next().value; // "yellow"
light.next().value; // "red"
light.next().value; // "green" (loops {lặp lại})

// A checkout wizard {Trình hướng dẫn thanh toán}
function* checkoutFlow() {
  const cart = yield "show-cart";
  const address = yield "enter-address";
  const payment = yield "enter-payment";
  return { cart, address, payment };
}

7. Pagination with Async Generators {Phân trang với Async Generator}

Hide pagination logic behind a clean iterator {Giấu logic phân trang sau một iterator gọn gàng}:

async function* paginate(endpoint) {
  let page = 1;
  while (true) {
    const res = await fetch(`${endpoint}?page=${page}`);
    const { items, hasMore } = await res.json();
    yield* items;          // yield each item individually {yield từng item}
    if (!hasMore) break;
    page++;
  }
}

// The consumer doesn't know or care about pages
// {Bên tiêu thụ không biết và không cần quan tâm tới trang}
let count = 0;
for await (const item of paginate("/api/products")) {
  process(item);
  if (++count >= 100) break; // stop early — no extra fetches {dừng sớm — không fetch thừa}
}

8. Streaming Chunks (Fetch Reader) {Stream từng khối (Fetch Reader)}

Read a large response incrementally instead of buffering it all {Đọc response lớn từng phần thay vì buffer toàn bộ}:

async function* streamLines(url) {
  const res = await fetch(url);
  const reader = res.body.getReader();
  const decoder = new TextDecoder();
  let buffer = "";

  while (true) {
    const { value, done } = await reader.read();
    if (done) break;
    buffer += decoder.decode(value, { stream: true });

    let newlineIndex;
    while ((newlineIndex = buffer.indexOf("\n")) >= 0) {
      yield buffer.slice(0, newlineIndex);
      buffer = buffer.slice(newlineIndex + 1);
    }
  }
  if (buffer) yield buffer; // last line {dòng cuối}
}

// Process a huge log file line-by-line {Xử lý file log khổng lồ từng dòng}
for await (const line of streamLines("/logs/huge.txt")) {
  if (line.includes("ERROR")) console.log(line);
}

9. Two-Way Coroutine (redux-saga style) {Coroutine hai chiều (kiểu redux-saga)}

Generators let a “runner” drive side effects {Generator cho phép một “runner” điều khiển side effect} while the generator stays pure {trong khi generator vẫn thuần khiết}. This is exactly how redux-saga works {Đây chính xác là cách redux-saga hoạt động}:

// Effects are plain objects — easy to test {Effect là object thuần — dễ test}
const call = (fn, ...args) => ({ type: "CALL", fn, args });

function* loginSaga() {
  const user = yield call(fetchUser, 1);     // describe an effect {mô tả một effect}
  const posts = yield call(fetchPosts, user.id);
  return { user, posts };
}

// The runner interprets effects and feeds results back in
// {Runner diễn giải effect và đưa kết quả trở lại}
async function runSaga(saga) {
  const it = saga();
  let result = it.next();
  while (!result.done) {
    const effect = result.value;
    if (effect.type === "CALL") {
      const value = await effect.fn(...effect.args);
      result = it.next(value); // feed the result back {đưa kết quả trở lại}
    }
  }
  return result.value;
}

The generator describes what to do {Generator mô tả cái gì cần làm}; the runner decides how {runner quyết định làm thế nào}. This separation makes the logic trivially testable {Sự tách biệt này khiến logic cực dễ test}.

10. Cooperative Scheduling — Chunking Heavy Loops {Lập lịch hợp tác — chia nhỏ vòng lặp nặng}

A long synchronous loop freezes the UI {Một vòng lặp đồng bộ dài làm đơ UI}. A generator lets you process in chunks and yield control back to the browser {Generator cho phép bạn xử lý từng khối và trả quyền điều khiển lại cho browser}:

function* processInChunks(items, chunkSize = 500) {
  for (let i = 0; i < items.length; i += chunkSize) {
    const chunk = items.slice(i, i + chunkSize);
    for (const item of chunk) heavyWork(item);
    yield i + chunk.length; // progress so far {tiến độ hiện tại}
  }
}

function runCooperatively(items) {
  const it = processInChunks(items);

  function step() {
    const { value, done } = it.next();
    if (done) return;
    updateProgressBar(value / items.length);
    // Hand control back so the browser can paint/respond
    // {Trả quyền điều khiển để browser có thể paint/phản hồi}
    requestIdleCallback(step);
  }
  step();
}

runCooperatively(hugeArray); // UI stays responsive {UI vẫn mượt}

Bonus Patterns {Pattern thưởng thêm}

Zipping Two Iterables {Ghép hai iterable}

function* zip(a, b) {
  const itA = a[Symbol.iterator]();
  const itB = b[Symbol.iterator]();
  while (true) {
    const x = itA.next();
    const y = itB.next();
    if (x.done || y.done) return;
    yield [x.value, y.value];
  }
}

console.log([...zip(["a", "b", "c"], [1, 2, 3])]);
// [["a", 1], ["b", 2], ["c", 3]]

Throttling a Stream of Events {Giới hạn luồng sự kiện}

async function* throttle(asyncIterable, ms) {
  let last = 0;
  for await (const value of asyncIterable) {
    const now = Date.now();
    if (now - last >= ms) {
      last = now;
      yield value;
    }
  }
}

Common Pitfalls {Các lỗi thường gặp}

Pitfall {Lỗi}Detail {Chi tiết}
Generators iterate once {lặp một lần}After exhaustion, for...of yields nothing — call the function again for a fresh one {Sau khi cạn, for...of không trả gì — gọi lại hàm để có cái mới}
No random access {Không truy cập ngẫu nhiên}You can’t do gen[5] — only sequential .next() {Không thể gen[5] — chỉ tuần tự .next()}
First .next(arg) ignores arg {.next(arg) đầu bỏ qua arg}No paused yield exists yet to receive it {Chưa có yield đang dừng để nhận}
return in for...of is ignored {return trong for...of bị bỏ qua}Use .next() manually if you need the return value {Dùng .next() thủ công nếu cần giá trị return}
Mixing sync/async {Trộn sync/async}for...of won’t await — use for await...of for async generators {for...of không await — dùng for await...of}
Forgetting cleanup {Quên dọn dẹp}Early break triggers the generator’s finally block — put cleanup there {break sớm kích hoạt khối finally — đặt dọn dẹp ở đó}
// finally runs even when the consumer breaks early
// {finally chạy ngay cả khi bên tiêu thụ break sớm}
function* withCleanup() {
  try {
    yield 1;
    yield 2;
    yield 3;
  } finally {
    console.log("cleanup!"); // closes files, sockets, etc. {đóng file, socket...}
  }
}

for (const x of withCleanup()) {
  if (x === 2) break; // logs "cleanup!" {vẫn chạy "cleanup!"}
}

Quick Reference {Tham khảo nhanh}

function* fn() {}      → declare a generator {khai báo generator}
yield x                → pause, output x {dừng, xuất x}
const v = yield x      → pause; v = value passed to next() {v = giá trị truyền vào next()}
yield* iterable        → delegate to another iterable {uỷ quyền}
gen.next(v)            → resume, v becomes the yield result {tiếp tục}
gen.return(v)          → force-finish with value v {kết thúc cưỡng bức}
gen.throw(e)           → inject error at current yield {ném lỗi}
for...of               → consume sync generator {tiêu thụ generator đồng bộ}
for await...of         → consume async generator {tiêu thụ generator bất đồng bộ}
[...gen]               → collect all values into array {gom mọi giá trị vào mảng}

When to reach for a generator {Khi nào dùng generator}:
- Infinite or very large sequences {chuỗi vô hạn / rất lớn}
- Lazy evaluation, avoid intermediate arrays {đánh giá lười}
- Custom iteration (trees, graphs, linked lists) {lặp tuỳ chỉnh}
- Streaming / pagination {stream / phân trang}
- State machines / step-by-step flows {máy trạng thái}
- Two-way coroutines (saga effects) {coroutine hai chiều}

Practice Exercises {Bài tập thực hành}

Try these to cement the concepts {Thử các bài này để củng cố khái niệm}:

  1. Fibonacci generator {Generator Fibonacci}: write function* fib() that yields 0, 1, 1, 2, 3, 5, 8... infinitely {yield vô hạn}. Then use take() to get the first 10 {rồi dùng take() lấy 10 số đầu}.
  2. chunk(iterable, size) — a generator that groups an iterable into arrays of size {nhóm iterable thành các mảng cỡ size}, e.g., chunk([1,2,3,4,5], 2)[1,2], [3,4], [5].
  3. enumerate(iterable) — yield [index, value] pairs like Python’s enumerate {yield cặp [index, value] như enumerate của Python}.
  4. Object entries walker {Bộ duyệt entries object}: a recursive generator that yields every [path, value] leaf of a nested object {yield mọi lá [path, value] của object lồng nhau} (e.g., { a: { b: 1 } }["a.b", 1]).
  5. Retry coroutine {Coroutine thử lại}: a generator-based runner that retries a failing async effect up to N times {thử lại effect async lỗi tối đa N lần}.

Generators are not exotic {Generator không phải thứ kỳ lạ} — once the pausable-function model clicks {một khi mô hình hàm-tạm-dừng “thông”}, you’ll start seeing problems they solve elegantly {bạn sẽ bắt đầu thấy những vấn đề chúng giải quyết một cách thanh lịch}: anywhere you have a sequence over time {bất cứ đâu bạn có chuỗi theo thời gian}, lazy data {dữ liệu lười}, or step-by-step control flow {hoặc luồng điều khiển từng bước}.