for await...of and Streaming Data
for await...of lets you loop over async iterables.
It is most useful when data arrives in pieces instead of all at once.
This is the mental model behind many streaming APIs.
Why Streaming Matters
Some data is small enough to load all at once.
const response = await fetch("/api/users");
const users = await response.json();But some data is large or continuous:
- a large file
- a long API response
- live chat messages
- logs
- sensor data
For these cases, processing chunks as they arrive can be more efficient.
The Basic Syntax
Use for await...of inside an async function or module.
async function printValues(asyncIterable) {
for await (const value of asyncIterable) {
console.log(value);
}
}Each loop step waits for the next value.
A Simple Async Stream
Here is an async generator that acts like a small stream.
async function* downloadChunks() {
yield "chunk 1";
await new Promise((resolve) => setTimeout(resolve, 300));
yield "chunk 2";
await new Promise((resolve) => setTimeout(resolve, 300));
yield "chunk 3";
}
for await (const chunk of downloadChunks()) {
console.log(chunk);
}Output:
chunk 1
chunk 2
chunk 3The values are handled as they arrive.
Streaming Instead of Waiting
Imagine a report with thousands of rows.
Waiting for every row before showing anything can make the app feel slow.
An async iterable lets the UI update gradually.
async function renderReport(rows) {
for await (const row of rows) {
addRowToTable(row);
}
}The function can display each row without needing the entire report in memory.
Reading Fetch Response Streams
Modern browsers expose response bodies as streams.
The exact APIs are more advanced, but the idea is familiar: read chunks until the stream is done.
async function readTextStream(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let result = "";
while (true) {
const { value, done } = await reader.read();
if (done) {
break;
}
result += decoder.decode(value, { stream: true });
}
result += decoder.decode();
return result;
}This example uses a ReadableStream reader directly.
Some runtimes and libraries wrap streams as async iterables, which makes them usable with for await...of.
Creating a Stream-Like Helper
You can wrap repeated async work in an async generator.
async function* pollUntilComplete(jobId) {
while (true) {
const response = await fetch(`/api/jobs/${jobId}`);
const job = await response.json();
yield job;
if (job.status === "complete" || job.status === "failed") {
return;
}
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}Now the caller can decide what to do with each status update.
for await (const job of pollUntilComplete("job_123")) {
updateProgress(job.progress);
}Breaking Early
You can stop a for await...of loop with break.
for await (const message of incomingMessages()) {
if (message.type === "error") {
break;
}
renderMessage(message);
}If the async generator has a finally block, it can clean up when the loop exits.
Sequential by Design
for await...of waits for each next value.
That is exactly what you want for streams, where order often matters.
But it is not the same as running many tasks in parallel.
for await (const file of filesToUpload()) {
await uploadFile(file);
}This uploads one file at a time.
If you need controlled parallel work, use a concurrency limit pattern later in this module.
Common Mistakes
Do not use Array.prototype.forEach() with await and expect it to pause the outer function.
items.forEach(async (item) => {
await saveItem(item);
});
console.log("This runs before saves finish");Use for...of or for await...of when you need sequential waiting.
for (const item of items) {
await saveItem(item);
}Do not stream if you need the entire result before doing anything.
For small data, simple await response.json() is often clearer.
Best Practices
- Use
for await...offor async values that arrive over time. - Keep stream processing incremental.
- Use
breakwhen you no longer need more values. - Handle errors around the whole loop.
- Choose sequential streaming deliberately; do not assume it is parallel.
Summary
for await...of consumes async iterables one value at a time.
It is a natural fit for streaming data, polling status updates, paginated results, and async sources where values arrive gradually.
Streaming patterns help apps respond sooner and avoid loading everything into memory at once.