denoland/deno · error · Error
ERR_HTTP2_TRAILERS_NOT_READY
ERR_HTTP2_TRAILERS_NOT_READY
Error message
Trailing headers cannot be sent until after the wantTrailers event is emitted
What it means
sendTrailers throws ERR_HTTP2_TRAILERS_NOT_READY until the stream signals readiness. The trailersReady flag is only set in onStreamTrailers, which runs when the stream finishes sending data AND the stream was created with { waitForTrailers: true } in respond()/respondWithFD()/respondWithFile() options — that is also what emits the 'wantTrailers' event. Calling sendTrailers before that moment (typically right after respond) is premature.
Source
Thrown at ext/node/polyfills/http2.ts:2472
this._readableState.readingMore = false;
this[kState].didRead = true;
}
if (!this.pending) {
FunctionPrototypeCall(streamOnResume, this);
} else {
this.once("ready", streamOnResume);
}
}
sendTrailers(headers) {
if (this.destroyed || this.closed) {
throw new ERR_HTTP2_INVALID_STREAM();
}
if (this[kSentTrailers]) {
throw new ERR_HTTP2_TRAILERS_ALREADY_SENT();
}
if (!this[kState].trailersReady) {
throw new ERR_HTTP2_TRAILERS_NOT_READY();
}
assertIsObject(headers, "headers");
headers = ObjectAssign({ __proto__: null }, headers);
debugStreamObj(this, "sending trailers");
this[kUpdateTimer]();
const headersList = buildNgHeaderString(
headers,
assertValidPseudoHeaderTrailer,
this.session[kStrictSingleValueFields],
);
this[kSentTrailers] = headers;
// Send the trailers in setImmediate so we don't do it on nghttp2 stack.
setImmediate(finishSendTrailers, this, headersList);View on GitHub (pinned to 9ad36f7a2c)
Solutions
- Pass { waitForTrailers: true } to respond(): stream.respond(headers, { waitForTrailers: true })
- Send trailers exclusively inside the 'wantTrailers' event listener, which fires exactly when the stream is ready
- Register the listener before respond/end so the auto empty-trailers path is not taken
Example fix
// before
stream.respond({ ':status': 200 });
stream.end(body);
stream.sendTrailers({ 'x-checksum': sum }); // throws TRAILERS_NOT_READY
// after
stream.on('wantTrailers', () => stream.sendTrailers({ 'x-checksum': sum }));
stream.respond({ ':status': 200 }, { waitForTrailers: true });
stream.end(body); Defensive patterns
Strategy: validation
Validate before calling
if (!respondedWithWaitForTrailers) {
throw new Error('pass { waitForTrailers: true } to respond() before sending trailers');
}
// readiness = the 'wantTrailers' event fired
stream.on('wantTrailers', () => stream.sendTrailers(trailerHeaders));
stream.respond(headers, { waitForTrailers: true });
stream.end(body); Try / catch
try {
stream.sendTrailers(headers);
} catch (err) {
const code = (err as NodeJS.ErrnoException).code;
if (code === 'ERR_HTTP2_TRAILERS_NOT_READY') {
// defer: retry from the 'wantTrailers' listener instead of now
} else throw err;
} Prevention
- Always pair sendTrailers with respond(..., { waitForTrailers: true }) and a 'wantTrailers' listener
- Never send trailers ad hoc after end(); use the event contract
- Encapsulate the pattern in one helper so call sites cannot get it wrong
When it happens
Trigger: stream.respond(headers) without waitForTrailers, then stream.sendTrailers(...) after end() — readiness never becomes true; calling sendTrailers immediately after respond() before the body finished; using respondWithFD and sending trailers before the file fully streamed.
Common situations: Code ported from HTTP/1 chunked trailers where you append trailers after the last chunk; developers assuming trailers can be queued any time after headers; forgetting the waitForTrailers opt-in entirely.
Related errors
- ERR_HTTP2_TRAILERS_ALREADY_SENT
- ERR_HTTP2_INVALID_STREAM
- ERR_HTTP2_NESTED_PUSH
- ERR_INVALID_HTTP_TOKEN
- ERR_INVALID_CHAR
AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20).
Data as JSON: /api/errors/c0fa1264da15797d.
Report an issue: GitHub.