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.
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.
Always use return after calling any reply method to prevent further execution:
// 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);
});Structure your handler logic to ensure only one code path sends a response:
// 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);
});Ensure all async operations finish before sending the response:
// 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
});Fastify hooks (onRequest, preSerialization, etc) can also send replies. Ensure they don't conflict:
// 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' });
});Use error handlers and try/catch to send one response for both success and error:
// 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);
});Add logging to trace when replies are sent:
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 issueFastify 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:
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:
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.
Error: EMFILE: too many open files, watch
EMFILE: fs.watch() limit exceeded
Error: Middleware next() called multiple times (next() invoked twice)
Express middleware next() called multiple times
Error: Worker failed to initialize (worker startup error)
Worker failed to initialize in Node.js
Error: EMFILE: too many open files, open 'file.txt'
EMFILE: too many open files
Error: cluster.fork() failed (cannot create child process)
cluster.fork() failed - Cannot create child process