Logo
Skip to main content
Development
7 min read

Expressjs UnhandledPromiseRejectionWarning: Error: Can't set headers after they are sent

D

Divya Mahi

March 11, 2024 · Updated March 11, 2024

Expressjs UnhandledPromiseRejectionWarning_ Error_ Can't set headers after they are sent.

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.

Development
D

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.

Continue Reading

Related Articles