Bootcamp backendontwikkeling met Node.js · Les

Async-iterators en for-await-of over streams

Verwerk Readable-streams eenvoudig met async-iteratie en zet streams om naar en vanuit async-generators.

Les 4 van 413 stappen

Async-iterators en for-await-of over streams is een gratis Bootcamp backendontwikkeling met Node.js-les op CoddyKit. Dit is les 4 van 4. Je kunt de volledige les hieronder gratis lezen en daarna in de browser praktisch oefenen met een ingebouwde code-editor en een AI-begeleider die 24/7 beschikbaar is. Deze les maakt deel uit van het leertraject Bootcamp backendontwikkeling met Node.js. Je voortgang wordt gesynchroniseerd op het web en in de CoddyKit-app. De cursus Bootcamp backendontwikkeling met Node.js bevat in totaal 4 lessen.

Waarom asynchrone iteratie over streams

Node.js Readable-streams geven in de loop van de tijd stukken gegevens uit. De klassieke manier om ze te consumeren is het koppelen van luisteraars voor de gebeurtenissen 'data', 'end' en 'error'. Dat werkt, maar verspreidt je logica over callbacks en maakt pauzeren, hervatten en foutafhandeling omslachtig.

Sinds Node 10 is elke Readable-stream een asynchrone iterable. Dat betekent dat je deze kunt consumeren met een gewone for await...of-lus:

  • Sequentiële verwerking van stukken gegevens, van boven naar beneden geschreven
  • Automatische tegendruk — de lus haalt één stuk tegelijk op
  • try/catch werkt voor streamfouten

In deze les leer je hoe je streams op een praktische manier consumeert, transformeert en produceert met asynchrone iteratie.

Basisprincipes van for-await-of

Een for await...of-lus wacht op elke waarde die door een asynchrone iterator wordt geproduceerd voordat hij doorgaat naar de volgende. Wanneer je deze op een Readable-stream toepast, levert elke iteratie één stuk op (standaard een Buffer, of een tekenreeks als de codering van de stream is ingesteld).

Hier lezen we het bronbestand van dit script stuk voor stuk. De lusbody wordt één keer per stuk uitgevoerd en de lus eindigt vanzelf wanneer de stream klaar is:

import { createReadStream } from 'node:fs';
import { fileURLToPath } from 'node:url';

const self = fileURLToPath(import.meta.url);
const stream = createReadStream(self, { encoding: 'utf8' });

let chunks = 0;
let bytes = 0;
for await (const chunk of stream) {
  chunks++;
  bytes += chunk.length;
}
console.log('chunks:', chunks, 'chars:', bytes);

Ingebouwde tegendruk

Het grootste voordeel van for await...of is automatische tegendruk. De lus vraagt het volgende stuk pas op wanneer de body van de vorige iteratie klaar is met wachten. Als je body traag asynchroon werk uitvoert (een schrijfbewerking naar een database of een HTTP-aanroep), wordt de stream gepauzeerd totdat je klaar bent.

Dit krijg je automatisch — je hoeft niet handmatig de dans van stream.pause() en stream.resume() uit te voeren. Dit gesimuleerde voorbeeld verwerkt elk stuk met een vertraging; de stream wacht tussen de iteraties:

import { Readable } from 'node:stream';
import { setTimeout as sleep } from 'node:timers/promises';

const source = Readable.from(['a', 'b', 'c', 'd']);

for await (const item of source) {
  // Slow consumer: stream is paused while we await
  await sleep(50);
  console.log('processed', item);
}
console.log('done');

Readable.from(): iterable naar stream

Readable.from() zet elke iterable of asynchrone iterable (arrays, generatoren en asynchrone generatoren) om in een echte Readable-stream. Dit vormt de verbinding tussen gewone gegevens of generatorlogica en het stream-ecosysteem (pipes, HTTP-antwoorden en schrijfbewerkingen naar bestanden).

Je kunt het resultaat van een generatorfunctie meegeven om waarden lui te produceren:

import { Readable } from 'node:stream';

function* counter(n) {
  for (let i = 1; i <= n; i++) {
    yield `line ${i}\n`;
  }
}

const stream = Readable.from(counter(3));

for await (const chunk of stream) {
  process.stdout.write(chunk);
}

Asynchrone generatoren als streambronnen

Readable.from() accepteert ook een asynchrone generator. Daarmee kun je streamgegevens produceren uit asynchrone bronnen — gepagineerde API's, taken in een wachtrij of gebeurtenissen op vaste tijdstippen — terwijl je overzichtelijke sequentiële code behoudt.

De asynchrone generator hieronder levert met korte tussenpozen een record op, en Readable.from() stelt dit beschikbaar als een standaardstream die elke Node-consument kan lezen:

import { Readable } from 'node:stream';
import { setTimeout as sleep } from 'node:timers/promises';

async function* fetchPages() {
  for (let page = 1; page <= 3; page++) {
    await sleep(20); // simulate async fetch
    yield { page, items: page * 2 };
  }
}

const stream = Readable.from(fetchPages(), { objectMode: true });

for await (const record of stream) {
  console.log('got page', record.page, 'items', record.items);
}

Streams in objectmodus

Standaard bevatten streams stukken van het type Buffer of tekenreeks. Wanneer je objectMode: true instelt, kan elk stuk elke willekeurige JavaScript-waarde zijn — een object, getal of array. Dit is essentieel wanneer je over gestructureerde records itereert in plaats van over ruwe bytes.

Readable.from() schakelt de objectmodus automatisch in wanneer de bron waarden oplevert die geen tekenreeks of Buffer zijn, maar expliciet zijn maakt de bedoeling duidelijk. Asynchrone iteratie levert je objecten vervolgens rechtstreeks op:

import { Readable } from 'node:stream';

const users = [
  { id: 1, name: 'Ada' },
  { id: 2, name: 'Linus' },
  { id: 3, name: 'Grace' },
];

const stream = Readable.from(users, { objectMode: true });

for await (const user of stream) {
  console.log(`#${user.id} -> ${user.name}`);
}

Transformeren met asynchrone generatoren

Je hebt niet altijd een Transform-streamklasse nodig om streamgegevens te projecteren of te filteren. Je kunt de bronstream omwikkelen met een asynchrone generator die zelf for await...of gebruikt en getransformeerde waarden oplevert. Dit kan op natuurlijke wijze worden gecombineerd en leest als synchrone code.

Hier parseert een generator regels in NDJSON-stijl naar objecten en filtert hij deze, terwijl de tegendruk behouden blijft:

import { Readable } from 'node:stream';

async function* onlyActive(source) {
  for await (const record of source) {
    if (record.active) {
      yield record.name;
    }
  }
}

const input = Readable.from([
  { name: 'job-a', active: true },
  { name: 'job-b', active: false },
  { name: 'job-c', active: true },
], { objectMode: true });

for await (const name of onlyActive(input)) {
  console.log('active:', name);
}

Fouten afhandelen met try/catch

Een groot voordeel voor de schrijfbaarheid: streamfouten verschijnen als gegooide uitzonderingen in de lus, zodat een normale try/catch ze afhandelt. Bij gebeurtenisgestuurde consumptie moest je afzonderlijk naar 'error' luisteren en liep je het risico op niet-afgehandelde afwijzingen.

Als de stream tijdens een iteratie 'error' uitzendt, wordt het wachten op next() afgewezen en springt de uitvoering naar catch:

import { Readable } from 'node:stream';

async function* faulty() {
  yield 1;
  yield 2;
  throw new Error('source blew up');
}

const stream = Readable.from(faulty(), { objectMode: true });

try {
  for await (const n of stream) {
    console.log('value', n);
  }
} catch (err) {
  console.error('caught:', err.message);
}

Een vroege break vernietigt de stream

Wanneer je met break, return of throw uit een for await...of-lus gaat, roept Node de return()-methode van de iterator aan. Die vernietigt de onderliggende stream en geeft de bijbehorende bronnen vrij (bestandsdescriptors en sockets). Dit is een belangrijke garantie voor het opruimen.

Als je dus na de eerste overeenkomst stopt, blijft de geopende bestandsingang niet lekken — de stream wordt automatisch afgesloten:

import { Readable } from 'node:stream';

const stream = Readable.from(['alpha', 'beta', 'gamma', 'delta']);

for await (const word of stream) {
  console.log('checking', word);
  if (word === 'beta') {
    console.log('found it, stopping');
    break; // stream is destroyed on break
  }
}
console.log('destroyed?', stream.destroyed);

De body van een HTTP-verzoek consumeren

In een Node-backend is een binnenkomend HTTP-verzoek (req) een Readable-stream. Met asynchrone iteratie kun je de body van het verzoek overzichtelijk verzamelen of verwerken zonder alles handmatig te bufferen.

Dit patroon verzamelt stukken en parseert ze daarna als JSON. Omdat dit servercode is (de code koppelt een poort en heeft een HTTP-client nodig), is het voorbeeld hier ter illustratie en kan het niet zelfstandig worden uitgevoerd:

import { createServer } from 'node:http';

const server = createServer(async (req, res) => {
  try {
    const chunks = [];
    for await (const chunk of req) {
      chunks.push(chunk);
    }
    const body = Buffer.concat(chunks).toString('utf8');
    const data = JSON.parse(body || '{}');
    res.end(JSON.stringify({ received: data }));
  } catch (err) {
    res.statusCode = 400;
    res.end('bad request');
  }
});

server.listen(3000);

Iteratorhulpmiddelen en stream.compose()

Moderne versies van Node voegen rechtstreeks aan Readable-streams hulpmiddelen van een hoger niveau toe die intern de asynchrone iterator consumeren: stream.map(), stream.filter(), stream.toArray(), stream.take() en meer. Ze behouden de tegendruk en voorkomen dat je lussen met de hand moet schrijven.

Je kunt ze vloeiend aan elkaar koppelen. toArray() leegt de stream en levert uiteindelijk een gewone array op:

  • .map(fn) — transformeert elk stuk (ondersteunt een asynchrone fn en gelijktijdigheid)
  • .filter(fn) — behoudt overeenkomende stukken
  • .toArray() — verzamelt alles in een array
import { Readable } from 'node:stream';

const result = await Readable.from([1, 2, 3, 4, 5])
  .filter((n) => n % 2 === 1)
  .map((n) => n * 10)
  .toArray();

console.log(result); // [10, 30, 50]

Korte controle

Je consumeert een Readable-bestandsstream met for await...of en gaat met break uit de lus nadat je de eerste overeenkomende regel hebt gevonden. Wat gebeurt er met de onderliggende bestandsbron?

Samenvatting

Je hebt geleerd hoe je Node-streams consumeert en produceert met asynchrone iteratie:

  • for await...of leest een Readable één stuk tegelijk, met ingebouwde tegendruk en foutafhandeling met try/catch.
  • Readable.from() zet arrays, generatoren en asynchrone generatoren om in streams; gebruik objectMode voor gestructureerde records.
  • Wikkel een bron in een asynchrone generator om te projecteren of te filteren met behoud van tegendruk — een Transform-klasse is niet nodig.
  • break/return/throw vernietigt de stream en maakt bronnen automatisch vrij.
  • Hulpmiddelen zoals .map(), .filter() en .toArray() bieden een vloeiend alternatief voor handmatige lussen.

Gebruik asynchrone iteratie wanneer je in je backend streams sequentieel, leesbaar en zonder lekken wilt consumeren.

Gratis beginnen

Leer JavaScript met een AI-tutor — gratis

Schrijf echte code en voer die uit in je browser, krijg direct hulp van een AI-tutor die 24/7 beschikbaar is en ga verder waar je gebleven bent op het web of in de app.

Cursussen
22
Lessen
92

Veelgestelde vragen

Is de les “Async-iterators en for-await-of over streams” gratis?

Ja — de volledige tekst van “Async-iterators en for-await-of over streams” kun je hier gratis op het web lezen. Als je interactief wilt oefenen met een ingebouwde code-editor en een AI-begeleider die 24/7 beschikbaar is, en de rest van de cursus Bootcamp backendontwikkeling met Node.js wilt ontgrendelen, kun je upgraden naar CoddyKit PRO. De cursus Bootcamp backendontwikkeling met Node.js bevat in totaal 4 lessen.

Wat leer ik in “Async-iterators en for-await-of over streams”?

Verwerk Readable-streams eenvoudig met async-iteratie en zet streams om naar en vanuit async-generators. Je oefent met Bootcamp backendontwikkeling met Node.js door code rechtstreeks in de browser uit te voeren. Een AI-begeleider die 24/7 beschikbaar is beantwoordt je vragen terwijl je de les doorwerkt.

Heb ik ervaring nodig om met Bootcamp backendontwikkeling met Node.js te beginnen?

Ervaring vooraf is niet nodig. Bootcamp backendontwikkeling met Node.js op CoddyKit is opgebouwd voor beginners tot gevorderden, zodat je hier of bij het begin kunt starten en in je eigen tempo kunt leren. Dit is les 4 van 4.

Hoe lang duurt de les “Async-iterators en for-await-of over streams”?

De meeste lessen van CoddyKit duren ongeveer 5–10 minuten. Elke les is kort en interactief, zodat je gestaag vooruitgaat en op het web en in de app precies verdergaat waar je was gebleven.

Kan ik code schrijven en uitvoeren in deze les over Bootcamp backendontwikkeling met Node.js?

Ja. Elke les over Bootcamp backendontwikkeling met Node.js bevat een ingebouwde code-editor, zodat je rechtstreeks in je browser echte code kunt schrijven en uitvoeren en direct feedback van AI krijgt — lokale installatie is niet nodig.

Alle lessen in deze cursus

  1. Internals van readable-, writable-, duplex- en transformstreams
  2. Aangepaste transformstreams implementeren met _transform en _flush
  3. Backpressure, pipe() en het pipeline()-hulpprogramma
  4. Async-iterators en for-await-of over streams
← Terug naar Bootcamp backendontwikkeling met Node.js