Bootcamp i backendutvikling med Node.js · leksjon

Kontekstpropagering med AsyncLocalStorage

Ta med forespørselsspesifikk tilstand på tvers av asynkrone grenser uten å sende den gjennom hele kallestakken, ved hjelp av AsyncLocalStorage.

Leksjon 4 av 413 trinn

Kontekstpropagering med AsyncLocalStorage er en gratis leksjon i Bootcamp i backendutvikling med Node.js på CoddyKit. Dette er leksjon 4 av 4. Du kan lese hele leksjonen gratis nedenfor – og deretter øve praktisk i nettleseren med en innebygd kodeeditor og en AI-veileder som er tilgjengelig døgnet rundt. Den er en del av læringsløpet i Bootcamp i backendutvikling med Node.js, og fremdriften din synkroniseres mellom nettet og CoddyKit-appen. Kurset i Bootcamp i backendutvikling med Node.js inneholder totalt 4 leksjoner.

Problemet med prop-drilling

I en backend-tjeneste trengs data som gjelder en bestemt forespørsel, for eksempel en forespørsels-ID, den autentiserte brukeren eller en tenant-ID, dypt nede i kallstakken: i repositories, loggere og utgående HTTP-klienter.

Den naive løsningen er prop drilling — å sende et ctx-argument gjennom hver funksjon:

  • Signaturen til hver funksjon blir fylt opp med en ctx-parameter.
  • Én glemt videreføring gjør at et nedstrøms kall mister konteksten.
  • Bibliotekskode du ikke eier, kan ikke motta ctx i det hele tatt.

Vi trenger en måte å bære tilstand per forespørsel implisitt gjennom alle await-uttrykk og callback-funksjoner. Det er nettopp dette AsyncLocalStorage tilbyr.

async function handler(ctx, req) {
  const user = await loadUser(ctx, req.userId);
  return await renderPage(ctx, user);
}

async function loadUser(ctx, id) {
  log(ctx, 'loading user');         // ctx passed again
  return await db.find(ctx, id);    // and again...
}

// Every layer must accept and forward ctx by hand.

Hva AsyncLocalStorage er

AsyncLocalStorage ligger i den innebygde modulen node:async_hooks. Tenk på den som trådlokal lagring for den asynkrone verdenen: et lager som forblir knyttet til en logisk kjede av asynkrone operasjoner.

Du kaller als.run(store, callback) for å opprette et lager. Deretter vil als.getStore() returnere det samme lageret hvor som helst inne i callback-funksjonen — uansett hvor mange await-uttrykk, Promise-kjeder, setTimeout-kall eller event emitters som ligger imellom.

  • Hver samtidige forespørsel får sitt eget isolerte lager.
  • Ingen globale variabler og ingen kappløpssituasjoner mellom forespørsler.
  • Bygger under panseret på Nodes sporing av asynkron kontekst.
import { AsyncLocalStorage } from 'node:async_hooks';

const als = new AsyncLocalStorage();

als.run({ requestId: 'abc-123' }, () => {
  setTimeout(() => {
    const store = als.getStore();
    console.log(store.requestId); // 'abc-123'
  }, 10);
});

run() oppretter en kontekst

Kjerne-API-et er als.run(store, fn, ...args). Det kjører fn synkront, men knytter store til hele det asynkrone treet som opprettes fra det.

  • store kan være hvilken som helst verdi — oftest et vanlig objekt eller en Map.
  • run() returnerer det fn returnerer (inkludert en Promise).
  • Utenfor callback-funksjonen returnerer getStore() undefined.

Dette kodeeksempelet viser at lageret overlever et await-uttrykk på en ekte tidtaker.

import { AsyncLocalStorage } from 'node:async_hooks';
import { setTimeout as sleep } from 'node:timers/promises';

const als = new AsyncLocalStorage();

async function deep() {
  await sleep(5);
  return als.getStore()?.requestId;
}

await als.run({ requestId: 'r-42' }, async () => {
  const id = await deep();
  console.log('inside run:', id);   // 'r-42'
});

console.log('outside run:', als.getStore()); // undefined

Koble det inn i en HTTP-server

Mønsteret i en ekte server er å pakke arbeidet for hver forespørsel inn i als.run() helt ytterst, og starte lageret med en ny forespørsels-ID. Alt nedstrøms kan deretter lese den.

Eksempelet nedenfor bruker bare den innebygde modulen node:http, så det er ingen avhengighet til et rammeverk. Hver innkommende forespørsel får sitt eget lager, isolert fra samtidige forespørsler.

import http from 'node:http';
import { randomUUID } from 'node:crypto';
import { AsyncLocalStorage } from 'node:async_hooks';

const als = new AsyncLocalStorage();

function currentRequestId() {
  return als.getStore()?.requestId ?? 'no-context';
}

const server = http.createServer((req, res) => {
  als.run({ requestId: randomUUID() }, async () => {
    // deep call needs no ctx argument
    await Promise.resolve();
    res.end('request id: ' + currentRequestId());
  });
});

server.listen(3000, () => console.log('listening on 3000'));

Mønster for Express-middleware

I Express er det idiomatiske stedet å kalle als.run() en middleware som monteres først. Den må kalle next() inne i callback-funksjonen, slik at resten av kjeden arver lageret.

  • Les en innkommende x-request-id-header hvis en proxy eller oppstrøms tjeneste har sendt en; ellers genererer du en ny UUID.
  • Alle senere handlere, tjenester og loggere kan lese lageret uten å motta det som et argument.
import { AsyncLocalStorage } from 'node:async_hooks';
import { randomUUID } from 'node:crypto';

export const als = new AsyncLocalStorage();

export function contextMiddleware(req, res, next) {
  const store = {
    requestId: req.headers['x-request-id'] || randomUUID(),
    startedAt: Date.now(),
  };
  als.run(store, () => next()); // next() runs inside the context
}

export function getStore() {
  const store = als.getStore();
  if (!store) throw new Error('No request context');
  return store;
}

Strukturert logging med kontekst

Den største gevinsten: en logger som automatisk setter forespørsels-ID-en på hver linje – uten at kalleren noen gang sender den med.

Loggeren leser selv fra store-et. Hvis det ikke finnes en aktiv kontekst (for eksempel i oppstartskode), håndterer den dette på en kontrollert måte.

import { AsyncLocalStorage } from 'node:async_hooks';

const als = new AsyncLocalStorage();

function log(level, msg, extra = {}) {
  const store = als.getStore();
  const line = {
    ts: new Date().toISOString(),
    level,
    msg,
    requestId: store?.requestId ?? null,
    ...extra,
  };
  console.log(JSON.stringify(line));
}

als.run({ requestId: 'req-7' }, () => {
  log('info', 'user fetched', { userId: 99 });
});

log('warn', 'no request context here');

Endre store-et under en forespørsel

Fordi store-et er en referanse (et objekt eller en Map), kan De berike det etter at autentiseringen er ferdig. Senere logglinjer tar automatisk med de nye feltene.

  • Start med minimale data ved kanten (forespørsels-ID).
  • Etter at autentiserings-middleware er kjørt, legger De userId og tenantId til i det samme store-objektet.
  • Foretrekk en Map hvis De ønsker et tydelig nøkkelbasert API; et vanlig objekt fungerer også og er litt raskere.
import { AsyncLocalStorage } from 'node:async_hooks';

const als = new AsyncLocalStorage();

function set(key, value) {
  const store = als.getStore();
  if (store) store.set(key, value);
}
function get(key) {
  return als.getStore()?.get(key);
}

als.run(new Map([['requestId', 'r-1']]), () => {
  // ... later, after authenticating:
  set('userId', 42);
  set('tenantId', 'acme');
  console.log(get('requestId'), get('userId'), get('tenantId'));
});

enterWith kontra run

Det finnes to måter å angi et store på:

  • als.run(store, fn) – begrenser store-et til fn og dets asynkrone etterkommere. Når fn er ferdig, forsvinner konteksten. Foretrekk dette.
  • als.enterWith(store) – angir store-et for den gjeldende synkrone kjøringen og alt som følger etter i den samme asynkrone ressursen, uten automatisk avslutning.

enterWith må brukes med varsomhet: hvis De kaller den i en langlivet asynkron ressurs, kan den lekke inn i senere, urelatert arbeid. Bruk run med mindre De har en spesifikk grunn (for eksempel at De ikke kan pakke inn en callback).

import { AsyncLocalStorage } from 'node:async_hooks';

const als = new AsyncLocalStorage();

// Scoped and safe — context ends with the callback:
als.run({ id: 'A' }, () => {
  console.log(als.getStore().id); // 'A'
});
console.log(als.getStore());      // undefined

// enterWith persists with no clear boundary — easy to leak:
als.enterWith({ id: 'B' });
console.log(als.getStore().id);   // 'B' (and stays set!)

Her kan konteksten brytes

AsyncLocalStorage følger innebygde promises, async/await, tidtakere og de fleste hendelsesemittere. Men konteksten kan gå tapt i noen situasjoner:

  • Arbeid som planlegges før run() – for eksempel kjører en connection pool eller kø som opprettes ved oppstart, callbackene sine utenfor et forespørsels-store.
  • Noen eldre biblioteker som samler eller gjenbruker ressurser på tvers av forespørsler, kan ta med seg et foreldet store.
  • Manuelt frakoblede callback-funksjoner som lagres i en global array og kalles senere.

Løsningen ved integrasjon med slik kode er als.bind(fn) (eller AsyncResource.bind), som tar et øyeblikksbilde av den gjeldende konteksten og bruker den på nytt hver gang funksjonen kalles senere.

import { AsyncLocalStorage } from 'node:async_hooks';

const als = new AsyncLocalStorage();
const queue = [];

als.run({ requestId: 'r-9' }, () => {
  // bind captures the current store for later execution
  queue.push(als.bind(() => {
    console.log('later:', als.getStore()?.requestId);
  }));
});

// Runs outside the run() callback, but context is preserved:
queue.forEach((fn) => fn()); // later: r-9

En gjenbrukbar kontekstmodul

I praksis sentraliserer De store-et i én liten modul, slik at resten av kodebasen bare importerer hjelpefunksjoner – og aldri berører AsyncLocalStorage-instansen direkte.

Dette holder API-et ryddig: runWithContext() ved kanten, og requestId() / getUser() alle andre steder.

import { AsyncLocalStorage } from 'node:async_hooks';
import { randomUUID } from 'node:crypto';

const als = new AsyncLocalStorage();

export function runWithContext(seed, fn) {
  const store = { requestId: randomUUID(), ...seed };
  return als.run(store, fn);
}

export function context() {
  return als.getStore() ?? null;
}

export function requestId() {
  return context()?.requestId ?? null;
}

export function setUser(user) {
  const store = als.getStore();
  if (store) store.user = user;
}

Ytelse og god praksis

AsyncLocalStorage i moderne Node (v16+) er basert på en effektiv innebygd implementasjon, og merbelastningen er liten – akseptabel for praktisk talt alle webarbeidsbelastninger. Følg likevel noen gode rutiner:

  • Bruk én langlivet AsyncLocalStorage-instans per formål, ikke én per forespørsel.
  • Hold store-et lite; det er tilstand med forespørselslevetid, ikke en hurtigbuffer.
  • Ikke lagre hemmeligheter som De ikke ønsker skal vises i logger som leser store-et.
  • Foretrekk run() fremfor enterWith() for å få automatisk opprydding.
  • Håndter alltid tilfellet der store-et er undefined, for kode som kan kjøre utenfor en forespørsel.

Hurtigsjekk

Test forståelsen Deres av hvordan De oppretter en forespørselskontekst på en trygg måte.

Oppsummering

De har lært hvordan De kan videreføre tilstand med forespørselslevetid på tvers av asynkrone grenser uten å sende den gjennom alle kall:

  • AsyncLocalStorage fra node:async_hooks er trådlokal lagring for den asynkrone verdenen.
  • als.run(store, fn) knytter et store til fn og alle dets asynkrone etterkommere; als.getStore() leser det hvor som helst; utenfor kjøringen er verdien undefined.
  • Koble det til én gang ved kanten (HTTP-serveren eller den første middleware-funksjonen), og legg inn en forespørsels-ID før De beriker store-et etter autentisering.
  • En kontekstbevisst logger kan automatisk sette forespørsels-ID-en på hver linje.
  • Foretrekk run() fremfor enterWith() for automatisk opprydding, og bruk als.bind() / AsyncResource.bind() for å bevare konteksten på tvers av frakoblede callback-funksjoner.
  • Hold store-et lite, håndter tilfeller uten kontekst, og sentraliser tilgangen i én modul.
Gratis å komme i gang

Lær deg JavaScript med en AI-veileder – gratis

Skriv og kjør ekte kode i nettleseren, få umiddelbar hjelp fra en AI-veileder som er tilgjengelig døgnet rundt, og fortsett der du slapp – på nettet eller i appen.

Kurs
22
Leksjoner
92

Ofte stilte spørsmål

Er leksjonen «Kontekstpropagering med AsyncLocalStorage» gratis?

Ja – hele teksten i «Kontekstpropagering med AsyncLocalStorage» er gratis å lese her på nettet. For å øve interaktivt med en innebygd kodeeditor og en AI-veileder som er tilgjengelig døgnet rundt, og for å låse opp resten av Bootcamp i backendutvikling med Node.js-kurset, kan du oppgradere til CoddyKit PRO. Kurset i Bootcamp i backendutvikling med Node.js inneholder totalt 4 leksjoner.

Hva lærer jeg i «Kontekstpropagering med AsyncLocalStorage»?

Ta med forespørselsspesifikk tilstand på tvers av asynkrone grenser uten å sende den gjennom hele kallestakken, ved hjelp av AsyncLocalStorage. Du øver på Bootcamp i backendutvikling med Node.js med praktisk kode som du kjører direkte i nettleseren, mens en AI-veileder som er tilgjengelig døgnet rundt, svarer på spørsmålene dine mens du jobber deg gjennom leksjonen.

Trenger jeg erfaring for å begynne med Bootcamp i backendutvikling med Node.js?

Ingen tidligere erfaring er nødvendig. Bootcamp i backendutvikling med Node.js på CoddyKit er lagt opp for både nybegynnere og viderekomne, så De kan begynne her eller helt fra start og lære i Deres eget tempo. Dette er leksjon 4 av 4.

Hvor lang tid tar leksjonen «Kontekstpropagering med AsyncLocalStorage»?

De fleste CoddyKit-leksjoner tar omtrent 5–10 minutter. Hver leksjon er kort og interaktiv, slik at De gjør jevne fremskritt og kan fortsette akkurat der De slapp – både på nettet og i appen.

Kan jeg skrive og kjøre kode i denne Bootcamp i backendutvikling med Node.js-leksjonen?

Ja. Alle Bootcamp i backendutvikling med Node.js-leksjoner har en innebygd kodeeditor, slik at De kan skrive og kjøre ekte kode direkte i nettleseren og få umiddelbar tilbakemelding fra AI – uten lokal konfigurering.

Alle leksjonene i dette kurset

  1. Strukturert logging med korrelasjons-ID-er
  2. Distribuert sporing med OpenTelemetry-spans
  3. Eksponering av applikasjonsmetrikker og RED-metoden
  4. Kontekstpropagering med AsyncLocalStorage
← Tilbake til Bootcamp i backendutvikling med Node.js