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}:
- 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}.
- 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— whatyieldgave back {thứ màyieldtrả về}done—falsewhile paused,truewhen finished {falsekhi đang dừng,truekhi 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...ofand spread ignore the finalreturnvalue {for...ofvà spread bỏ qua giá trịreturncuối} and stop atdone: 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 và .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}:
- Fibonacci generator {Generator Fibonacci}: write
function* fib()that yields0, 1, 1, 2, 3, 5, 8...infinitely {yield vô hạn}. Then usetake()to get the first 10 {rồi dùngtake()lấy 10 số đầu}. chunk(iterable, size)— a generator that groups an iterable into arrays ofsize{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].enumerate(iterable)— yield[index, value]pairs like Python’senumerate{yield cặp[index, value]nhưenumeratecủa Python}.- 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]). - 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}.