response.writeHead()

Writes the status line and response headers, returning the response so calls can be chained.

Signature
response.writeHead(statusCode[, statusMessage][, headers])
  • Added in Node.js v0.1.30

Parameters

ParameterTypeDescription
$statusCodenumberThree-digit HTTP status code, such as 200 or 404. Must be in the range [100, 999].
$statusMessageoptionalstringHuman-readable reason phrase. Omitted, Node uses the standard phrase from http.STATUS_CODES.
$headersoptionalObject | ArrayResponse headers. As an array it is a flat list — even offsets are names, odd offsets are values, the same shape as request.rawHeaders — not a list of tuples.

Return value

<http.ServerResponse>A reference to the ServerResponse, so writeHead(...).end(body) chains.

Errors

Throws RangeError if statusCode is outside [100, 999], TypeError if a header name or value contains invalid characters, and ERR_HTTP_HEADERS_SENT if the headers were already sent.

Changelog

  • PHP v14.14.0headers may now be passed as a flat array, which is the only way to send repeated header fields in one call.
  • PHP v11.10.0, v10.17.0Returns this, enabling the writeHead(...).end(body) chain.
  • PHP v5.11.0, v4.4.5A RangeError is thrown when statusCode is not a number in the range [100, 999].

response.writeHead() commits the status line and headers. After it, only the body can be written — nothing about the head can change.

writeHead() versus setHeader()

setHeader() stages a header in a mutable map that later code can read back with getHeader() or remove. writeHead() is the flush. If you call writeHead() without ever calling setHeader(), the values go straight onto the wire and are never cached — so getHeader() afterwards returns undefined even though the header was sent. When both are used, they merge and writeHead() wins.

What trips people up

  • Once only, before end(). A second call throws ERR_HTTP_HEADERS_SENT — usually a handler that wrote a response and then fell through to the error path because a return was missing.
  • write() and end() call it implicitly. Any header set after those has already missed the boat.
  • Content-Length is bytes. Use Buffer.byteLength(body), not body.length; Node verifies the declared length against what you actually send.
  • The array form is flat, not [[name, value], ...].

Example

Example · javascript
const http = require('node:http');

const server = http.createServer((req, res) => {
  const body = 'hello world';

  res
    .writeHead(200, {
      // Bytes, not characters -- Node checks this against what you send.
      'Content-Length': Buffer.byteLength(body),
      'Content-Type': 'text/plain; charset=utf-8',
    })
    .end(body);

  // writeHead() wrote straight to the socket without caching:
  console.log('getHeader:', res.getHeader('Content-Type')); // undefined
  console.log('headersSent:', res.headersSent);             // true
  console.log('statusCode:', res.statusCode);               // 200
});

server.listen(3000);
// getHeader: undefined
// headersSent: true
// statusCode: 200

When to use it

  • Return a 404 with the standard reason phrase from http.STATUS_CODES
  • Chain a status, Content-Type, and Content-Length into a single expression before end()
  • Send two Set-Cookie headers in one call using the flat array form
  • Emit a 304 Not Modified with an ETag and no body

Frequently asked questions

What causes "Cannot set headers after they are sent to the client"?

Something wrote the response twice — either two writeHead() calls, or a setHeader() after write()/end() implicitly flushed the headers. It is nearly always a missing return after an early response, or a callback that fires again after the handler already replied; guard with if (res.headersSent) while you hunt it down.

What is the difference between writeHead() and setHeader() in Node?

setHeader() stages a header you can still read back or remove, while writeHead() flushes the status line and headers to the socket. When both are used they merge with writeHead() winning; when only writeHead() is used the headers are never cached, so getHeader() returns undefined afterwards.

How do I send multiple Set-Cookie headers with writeHead()?

Pass the headers as a flat array instead of an object: ['Set-Cookie', 'a=1', 'Set-Cookie', 'b=2']. Even offsets are names and odd offsets are values. This form was added in Node v14.14.0; before it, you had to use res.setHeader('Set-Cookie', [...]) with an array value.

More examples

Merging with setHeader(), and repeated headers via the array form

The first server shows the merge rule: headers staged with setHeader() are combined with the writeHead() object, and on a conflict writeHead() takes precedence. The second shows why the array form was added in v14.14.0 — an object cannot carry two Set-Cookie keys, so the flat array is the only single-call way to emit repeated fields.

Example · javascript
const http = require('node:http');

const server = http.createServer((req, res) => {
  res.setHeader('Content-Type', 'text/html');
  res.setHeader('X-Foo', 'bar');

  // writeHead() headers win the collision: the response is text/plain,
  // while X-Foo survives because writeHead() did not mention it.
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('ok');
});

const cookieServer = http.createServer((req, res) => {
  // Flat array: even offsets are names, odd offsets are values.
  // This is the only way to send the same header field twice here.
  res.writeHead(200, [
    'Set-Cookie', 'session=abc; HttpOnly; Path=/',
    'Set-Cookie', 'theme=dark; Path=/',
    'Content-Type', 'text/plain',
  ]);
  res.end('two cookies set');
});

cookieServer.listen(3000);

Conditional GET and a custom reason phrase

The middle argument replaces the standard reason phrase, so the status line reads 'HTTP/1.1 200 Totally Fine' instead of '200 OK'. The comment on the return marks the single most common cause of ERR_HTTP_HEADERS_SENT: a branch that responds and then falls through into code that responds again.

Example · javascript
const http = require('node:http');
const crypto = require('node:crypto');

const body = JSON.stringify({ id: 7, name: 'widget' });
const etag = `"${crypto.createHash('sha1').update(body).digest('hex')}"`;

const server = http.createServer((req, res) => {
  if (req.headers['if-none-match'] === etag) {
    // 304 must not carry a body -- end() with no argument.
    res.writeHead(304, { ETag: etag }).end();
    console.log('304 sent, statusMessage:', res.statusMessage);
    return; // without this return, the next writeHead throws ERR_HTTP_HEADERS_SENT
  }

  res.writeHead(200, 'Totally Fine', {
    ETag: etag,
    'Content-Type': 'application/json',
    'Content-Length': Buffer.byteLength(body),
  }).end(body);
});

server.listen(3000);

Discussion

  • Be the first to comment on this lesson.