Tackling Express.js UnhandledPromiseRejectionWarning: Can't Set Headers After They're Sent
Introduction
Express.js developers often face a daunting error: "UnhandledPromiseRejectionWarning: Error: Can't set headers after they are sent." This error, while cryptic at first glance, usually points to a common issue within asynchronous operations. This featured blog post aims to demystify this error, providing insights into its causes, real-world scenarios, and actionable solutions to prevent it.
Understanding the Error
The "Can't set headers after they are sent" error occurs in Express.js when an attempt is made to modify the HTTP headers of a response after the response has already been sent to the client. This typically happens within asynchronous operations or incorrect middleware use, leading to multiple attempts to send a response.
app.get('/api/data', async (req, res) => {
const data = await fetchData(); // Might throw
res.json(data);
// If fetchData throws, Express sends error response
// Then this line also tries to send — double response!
});
Diving Deeper
At its core, this error signifies a breach in the HTTP protocol's rules, where headers must precede the body, and once the body starts sending, headers can no longer be altered. In Express.js, this usually results from trying to send a response or redirect after a response has already been initiated or completed.
Common Scenarios and Fixes with Example Code Snippets
Scenario 1: Double Response in Asynchronous Handlers
Problematic Code:
app.get('/data', async (req, res) => {
const user = await getUser(req.query.id);
res.json(user);
// Log analytics — accidentally sends another response
await logAnalytics(user);
res.json({ logged: true }); // Headers already sent!
});
Explanation: After sending data with res.send(data), attempting another response with res.redirect('/') triggers the error.
Solution:
app.get('/data', async (req, res) => {
try {
const user = await getUser(req.query.id);
res.json(user);
// Fire and forget analytics — don't send another response
logAnalytics(user).catch(console.error);
} catch (err) {
if (!res.headersSent) {
res.status(500).json({ error: err.message });
}
}
});
Explanation: Ensuring only one response is sent per request cycle, either data or a redirect in case of an error, prevents the error.
Scenario 2: Conditional Responses in Middleware
Problematic Code:
app.use((req, res, next) => {
if (!req.headers.authorization) {
res.status(401).json({ error: 'Unauthorized' });
}
next(); // Continues even after sending 401
});
Explanation: Middleware sends a response and calls next(), potentially leading to another response.
Solution:
app.use((req, res, next) => {
if (!req.headers.authorization) {
return res.status(401).json({ error: 'Unauthorized' });
}
next();
});
Explanation: Adding an else clause ensures next() is called only when no response has been sent, avoiding the error.
Scenario 3: Unhandled Promise Rejections
Problematic Code:
app.get('/data', (req, res) => {
fetchFromDB()
.then(data => res.json(data))
.then(() => fetchMore())
.then(more => res.json(more)); // Second response!
});
Explanation: Not properly handling promise rejections might leave a request hanging, followed by an attempt to send a response after a timeout or another event, leading to the error.
Solution:
app.get('/data', async (req, res) => {
try {
const data = await fetchFromDB();
const more = await fetchMore();
res.json({ ...data, ...more }); // Single response
} catch (err) {
res.status(500).json({ error: err.message });
}
});
Explanation: Handling promise rejections with a response ensures the request is properly concluded, preventing the error.
Scenario 4: Async/Await Without Proper Error Handling
Problematic Code:
app.get('/user', async (req, res) => {
const user = await db.findUser(req.query.id);
res.json(user);
// If db.findUser throws, unhandled promise rejection
});
Explanation: Lack of try-catch blocks in async functions can result in unhandled exceptions, potentially causing header modification attempts after a response.
Solution:
app.get('/user', async (req, res, next) => {
try {
const user = await db.findUser(req.query.id);
if (!user) {
return res.status(404).json({ error: 'Not found' });
}
res.json(user);
} catch (err) {
next(err); // Pass to error handler
}
});
Explanation: Incorporating try-catch blocks in async route handlers captures errors, allowing for controlled responses and preventing the error.
Scenario 5: Incorrect Error Handling in Promises
Problematic Code:
app.get('/items', (req, res) => {
getItems()
.then(items => {
if (items.length === 0) {
res.status(404).send('No items');
}
res.json(items); // Sends even after 404
});
});
Explanation: The second .then() after res.json(profile) mistakenly attempts to send another response with res.redirect('/dashboard').
Solution:
app.get('/items', (req, res) => {
getItems()
.then(items => {
if (items.length === 0) {
return res.status(404).json({ error: 'No items found' });
}
res.json(items);
})
.catch(err => res.status(500).json({ error: err.message }));
});
Explanation: Removing the incorrect .then() and ensuring error handling with .catch() prevents multiple responses, addressing the issue.
Scenario 6: Response in a Loop
Problematic Code:
app.get('/process', async (req, res) => {
const items = await getItems();
items.forEach(item => {
res.json(item); // Sends response for each item!
});
});
Explanation: The res.json() inside a loop tries to send multiple responses, one for each iteration.
Solution:
app.get('/process', async (req, res) => {
const items = await getItems();
const results = items.map(item => processItem(item));
res.json(results); // Single response with all results
});
Explanation: Sending the entire data list as a single response with res.json() outside the loop eliminates the error.
Scenario 7: Asynchronous Functions in Middleware
Problematic Code:
app.use(async (req, res, next) => {
const session = await validateSession(req.cookies.sid);
if (!session) {
res.status(401).send('Invalid session');
}
next(); // Executes even after 401 response
});
Explanation: If performAsyncOperation sends a response, calling next() afterwards can lead to another response, causing the error.
Solution:
app.use(async (req, res, next) => {
try {
const session = await validateSession(req.cookies.sid);
if (!session) {
return res.status(401).json({ error: 'Invalid session' });
}
req.session = session;
next();
} catch (err) {
return res.status(500).json({ error: 'Session validation failed' });
}
});
Explanation: Checking res.headersSent before calling next() ensures that the middleware proceeds only if no response has been sent, preventing the error.
Scenario 8: Race Conditions in Parallel Operations
Problematic Code:
app.get('/data', (req, res) => {
fetchA().then(a => res.json({ a }));
fetchB().then(b => res.json({ b }));
// Both resolve — two responses sent!
});
Explanation: Using Promise.all for parallel operations, followed by attempting to send multiple responses for each resolved promise, leads to the error.
Solution:
app.get('/data', async (req, res) => {
try {
const [a, b] = await Promise.all([fetchA(), fetchB()]);
res.json({ a, b }); // Single response with both results
} catch (err) {
res.status(500).json({ error: err.message });
}
});
Explanation: Combining all data into a single response object and using .catch() for error handling ensures only one response is sent, effectively preventing the error even when handling multiple asynchronous operations.
Strategies to Prevent Errors
One Response Rule: Ensure each request handler or middleware in your Express.js application sends exactly one response to the client.
Proper Error Handling: Use try-catch blocks in async functions and .catch() in Promises to handle errors gracefully.
Middleware Management: Be cautious with calling next() in middleware that might have already sent a response.
Best Practices
Consistent Response Patterns: Establish and adhere to consistent patterns for sending responses and handling errors within your application.
Logging and Monitoring: Implement logging for responses and errors to track and prevent instances where multiple responses might be attempted.
Code Reviews: Regularly review code, especially async logic and middleware, to ensure compliance with best practices and prevent common errors.
Conclusion
The "Express.js UnhandledPromiseRejectionWarning: Error: Can't set headers after they are sent" is a common pitfall that stems from trying to send multiple responses within a single request cycle. By understanding the underlying causes and implementing the provided strategies and best practices, developers can effectively prevent this error, leading to more robust and error-free Express.js applications.
Written by
Divya Mahi
Building innovative digital solutions at Poulima InfoTech. We specialize in web & mobile app development using React, Next.js, Flutter, and AI technologies.
Ready to Build Your Next Project?
Transform your ideas into reality with our expert development team. Let's discuss your vision.
