How to fix Error: FST_ERR_REP_ALREADY_SENT: Reply was already sent (reply.send called twice) in Node.js

Node.jsBEGINNERHIGHUpdated Dec 9, 2025· 169 views

This Fastify error occurs when you attempt to send a response twice on the same request. Once reply.send() has been called, the response is finalized and cannot be sent again. Ensure each route handler sends a response only once.

Quick fix

Fixes FST_ERR_REP_ALREADY_SENT

// Bad - continues after reply.send()
fastify.get('/user/:id', async (request, reply) => {
  if (!request.params.id) {
    reply.status(400).send({ error: 'ID required' });
  }
  // Code continues, tries to send another response
  const user = await getUser(request.params.id);
  reply.send(user);
});

// Good - return after reply method
fastify.get('/user/:id', async (request, reply) => {
  if (!request.params.id) {
    return reply.status(400).send({ error: 'ID required' });
  }
  const user = await getUser(request.params.id);
  return reply.send(user);
});
  1. Return immediately after sending a response
  2. Use if/else or early returns for conditional responses
  3. Verify async operations complete before sending
  4. Check hook execution order and reply status
node.js — error
// Bad - continues after reply.send()
fastify.get('/user/:id', async (request, reply) => {
if (!request.params.id) {
reply.status(400).send({ error: 'ID required' });
}
// Code continues, tries to send another response
const user = await getUser(request.params.id);
reply.send(user);
});
// Good - return after reply method
fastify.get('/user/:id', async (request, reply) => {
if (!request.params.id) {
return reply.status(400).send({ error: 'ID required' });
}
const user = await getUser(request.params.id);
return reply.send(user);
});
Error: FST_ERR_REP_ALREADY_SENT: Reply was already sent (reply.send called twice)

What this error means

The FST_ERR_REP_ALREADY_SENT error indicates that your Fastify route handler attempted to send a response multiple times. Fastify strictly enforces that each incoming request gets exactly one response. Once you call reply.send(), reply.sendFile(), reply.redirect(), or any other response method, the response is finalized and you cannot call these methods again on the same reply object. This error is a safety mechanism that prevents accidental logic bugs where multiple response attempts could occur due to race conditions, missing returns, or unexpected code flow.

Typical symptoms

  • Fastify server crashes with FST_ERR_REP_ALREADY_SENT error
  • Error appears when route handler calls reply.send() multiple times
  • Async operations in handlers cause duplicate response attempts
  • Conditional logic sends different responses but forgets to return after first send
  • Error handlers or middleware attempt to send after route already responded
  • Race conditions between callbacks cause double sending

Common causes

  1. Calling reply.send() multiple times in the same route handler
  2. Not using return statement after sending a response
  3. Async callbacks that send responses after initial response
  4. Error handling that doesn't check if response already sent
  5. Multiple middleware or plugin hooks all sending responses
  6. Race conditions between promise resolutions and reply sends

How to fix "Error: FST_ERR_REP_ALREADY_SENT: Reply was already sent (reply.send called twice)"

1Return immediately after sending a response

Always use return after calling any reply method to prevent further execution:

javascript
// Bad - continues after reply.send()
fastify.get('/user/:id', async (request, reply) => {
  if (!request.params.id) {
    reply.status(400).send({ error: 'ID required' });
  }
  // Code continues, tries to send another response
  const user = await getUser(request.params.id);
  reply.send(user);
});

// Good - return after reply method
fastify.get('/user/:id', async (request, reply) => {
  if (!request.params.id) {
    return reply.status(400).send({ error: 'ID required' });
  }
  const user = await getUser(request.params.id);
  return reply.send(user);
});
2Use if/else or early returns for conditional responses

Structure your handler logic to ensure only one code path sends a response:

javascript
// Bad - multiple response paths without returns
fastify.get('/data', async (request, reply) => {
  const data = await fetchData();
  if (data.status === 'error') {
    reply.status(500).send({ error: data.message });
  }
  reply.send(data);
});

// Good - if/else ensures only one path executes
fastify.get('/data', async (request, reply) => {
  const data = await fetchData();
  if (data.status === 'error') {
    return reply.status(500).send({ error: data.message });
  }
  return reply.send(data);
});
3Verify async operations complete before sending

Ensure all async operations finish before sending the response:

javascript
// Bad - race condition with setTimeout
fastify.get('/process', async (request, reply) => {
  setTimeout(() => {
    reply.send({ delayed: true });
  }, 100);

  const result = await processData();
  reply.send(result); // Might happen before or after timeout
});

// Good - await all async operations
fastify.get('/process', async (request, reply) => {
  const result = await processData();
  return reply.send(result);
});

// Or if you need delayed response
fastify.post('/async-task', async (request, reply) => {
  return reply.send({ taskId: 'xyz', status: 'processing' });
  // Don't continue trying to send after this
});
4Check hook execution order and reply status

Fastify hooks (onRequest, preSerialization, etc) can also send replies. Ensure they don't conflict:

javascript
// Bad - both hook and handler send
fastify.addHook('onRequest', async (request, reply) => {
  if (!request.headers.authorization) {
    reply.status(401).send({ error: 'Unauthorized' });
  }
  // Falls through to handler which also sends
});

fastify.get('/protected', async (request, reply) => {
  reply.send({ data: 'secret' });
});

// Good - return from hook if sending response
fastify.addHook('onRequest', async (request, reply) => {
  if (!request.headers.authorization) {
    return reply.status(401).send({ error: 'Unauthorized' });
  }
});

fastify.get('/protected', async (request, reply) => {
  return reply.send({ data: 'secret' });
});
5Handle errors without double-sending

Use error handlers and try/catch to send one response for both success and error:

javascript
// Bad - catches error but handler may have already sent
fastify.get('/risky', async (request, reply) => {
  try {
    const result = await riskyOperation();
    reply.send(result);
  } catch (error) {
    reply.status(500).send({ error: error.message }); // Duplicate send
  }
});

// Good - return in try block
fastify.get('/risky', async (request, reply) => {
  try {
    const result = await riskyOperation();
    return reply.send(result);
  } catch (error) {
    return reply.status(500).send({ error: error.message });
  }
});

// Or use Fastify error handler
fastify.setErrorHandler((error, request, reply) => {
  return reply.status(500).send({ error: error.message });
});

fastify.get('/risky', async (request, reply) => {
  const result = await riskyOperation();
  return reply.send(result);
});
6Debug with logging to identify duplicate sends

Add logging to trace when replies are sent:

javascript
fastify.addHook('onSend', async (request, reply, payload) => {
  console.log(`[onSend] Sending reply for ${request.method} ${request.url}`);
  return payload;
});

fastify.get('/test', async (request, reply) => {
  console.log('[Handler] About to send response');
  return reply.send({ ok: true });
});

// If you see onSend called twice for same request, you have a double-send issue

Advanced notes

Fastify Reply Lifecycle: Once reply.send() is called, Fastify marks the reply as sent. The FST_ERR_REP_ALREADY_SENT check is strict because HTTP requests can only have one response.

Hook Execution Order: Fastify hooks execute in this order: onRequest → preHandler → preValidation → preSerialization → onSend → onResponse. Each hook or handler should send at most one response.

Streaming Responses: With reply.raw (raw Node.js response), you can write multiple chunks using res.write(), but each request still only sends one response. Use res.end() once:

javascript
fastify.get('/stream', (request, reply) => {
  reply.raw.write('chunk 1\n');
  reply.raw.write('chunk 2\n');
  reply.raw.end(); // Only once
});

Plugin and Middleware Context: If multiple plugins or route handlers process the same request, ensure only one sends a response. Use early returns and clear control flow.

Testing Fastify Routes: When testing, verify route handlers only send one response:

javascript
it('should send exactly one response', async () => {
  const response = await fastify.inject({
    method: 'GET',
    url: '/test'
  });
  expect(response.statusCode).toBe(200);
  // Framework prevents FST_ERR_REP_ALREADY_SENT before it reaches client
});

Comparison with Express: Express uses res.headersSent to check if headers were sent. Fastify is stricter with its reply object and throws an error immediately if you attempt a second send. This is more explicit and catches bugs faster.

Frequently asked questions

What does "FST_ERR_REP_ALREADY_SENT" mean?
The FST_ERR_REP_ALREADY_SENT error indicates that your Fastify route handler attempted to send a response multiple times. Fastify strictly enforces that each incoming request gets exactly one response. redirect(), or any other response method, the response is finalized and you cannot call these methods again on the same reply object.
How do I fix "FST_ERR_REP_ALREADY_SENT"?
This Fastify error occurs when you attempt to send a response twice on the same request. Once reply.send() has been called, the response is finalized and cannot be sent again. Ensure each route handler sends a response only once.
What causes "FST_ERR_REP_ALREADY_SENT"?
Calling reply.send() multiple times in the same route handler Not using return statement after sending a response Async callbacks that send responses after initial response
Is the "FST_ERR_REP_ALREADY_SENT" error serious?
Yes. This is a high-severity error that usually blocks work until it is fixed, but the steps above resolve it.

Official resources & further reading