Async Stack Traces: Warum `Error.stack` dich anlügt
Die Microtask-Queue hat meine Hausaufgaben gefressen (und meinen Debugging-Kontext).
Es ist 2 Uhr nachts. Der PagerDuty-Alarm heult.
Du öffnest die Logs und siehst das:
Error: Cannot read properties of undefined (reading 'id') at processTicksAndRejections (node:internal/process/task_queues:96:5)Das war’s. Kein Funktionsname. Keine Zeilennummer. Kein Dateipfad. Nur “processTicksAndRejections”.
Willkommen bei async JavaScript, wo die Stack Traces erfunden sind und die Zeilennummern keine Rolle spielen.
Warum Stack Traces kaputtgehen
Bei synchronem Code ist der Call Stack eine schöne Genealogie. A rief B, B rief C. Wenn C abstürzt, siehst du genau, wie du dorthin gekommen bist.
Bei async-Code (async/await) ist jedes await-Schlüsselwort ein Aussetzungspunkt.
Wenn du await verwendest, wird deine Funktion vom Stack gerissen. Sie wird in einen kryogenen Gefrierschrank namens Microtask Queue gesteckt. Der Stack ist jetzt leer (oder macht etwas anderes).
Wenn der Promise aufgelöst wird, wird deine Funktion aufgetaut und zurück auf den Stack geworfen. Aber die Geschichte ist weg.
Die Engine hat keine Ahnung, wer vor 500 Millisekunden await aufgerufen hat. Sie weiß nur, dass sie eine Aufgabe auszuführen hat.
V8s Versuche, das zu beheben
Node.js versucht zu helfen. Wir haben:
Error.captureStackTrace(): Erfasst den Stack bei der Erstellung. Nutzlos, wenn der Fehler später geworfen wird.--async-stack-traces: Ein Flag, das Node.js veranlasst, einen “Shadow Stack” von Promise-Ketten zu führen.- Der Preis: Es macht deine App 30 % langsamer.
- Das Ergebnis: Es hilft, aber es wird schnell unübersichtlich.
Die echte Lösung: AsyncLocalStorage
Wenn du in Production überleben willst, hör auf, Stack Traces anzusehen. Schau dir die Kausalität an.
Wir müssen Kontext (User-ID, Request-ID) an den “Thread” der Ausführung anhängen, auch wenn er zwischen Stack und Microtask Queue hin- und herspringt.
Node.js hat dafür ein eingebautes Tool: AsyncLocalStorage.
import { AsyncLocalStorage } from 'async_hooks';
const context = new AsyncLocalStorage();
// 1. Wrap the requestcontext.run({ requestId: '123' }, () => { // 2. Call deep async code await processOrder();});
// 3. Deep inside processOrder:async function processOrder() { await db.query();
// Magic! We can still see the requestId const { requestId } = context.getStore(); console.log(`[${requestId}] Failed to process order`);}Es ist egal, wie viele awaits dazwischen passieren. Der Kontext überlebt.
Production-Playbook
- Vertrau
err.stacknicht mehr. Er ist von Natur aus unvollständig. - Verwende strukturiertes Logging. Hänge
requestIdan jede einzelne Log-Zeile mitAsyncLocalStorage. - Trace, nicht Stack. Verwende OpenTelemetry. Es visualisiert die kausale Kette über Services hinweg – das ist es, worauf es wirklich ankommt.
Dein Code ist async. Dein Debugging-Kontext sollte es nicht sein.