/ โ† HTTP Error Fixes / Node.js Errors
In this guide
  1. Why Node apps fail differently
  2. The fix, step by step
  3. A safe Express error set-up
  4. Next.js status codes in practice
  5. Reading PM2 when the site shows 502
  6. Hosted platforms and their limits
  7. Preventing repeat failures
  8. Questions and answers

Why Node apps fail differently

A PHP site starts a fresh worker for each request, so a fatal error usually spoils only that one page. A Node app is one long-running process that handles every request. That design is fast, but it changes what errors do:

Because the audit sees the outside of the app, the first job is always to compare what the HTTP Error Audit reports with what your code thinks it is doing.

The fix, step by step

Step 1

Check the status codes your app really sends

Run the HTTP Error Audit and note every 404, 500 and 502. Then test the cases a crawl rarely reaches: request a made-up path such as /no-such-page-4821, a product or post ID that does not exist, and an API route with bad input, each with curl -s -o /dev/null -w "%{http_code}\n". Write down the code each one returns. Any "not found" page answering 200, or any error answering 200 with an error message in the body, needs fixing in the next steps.

Step 2

Return a real 404 for unknown routes and missing records

Two kinds of 404 need handling. Unknown paths need a catch-all at the end of your routing, such as a final app.use() in Express that sets res.status(404) and renders your not-found page. Known routes with a missing record, such as /products/:slug where the slug is not in the database, need the handler itself to check the lookup result and return 404 before rendering. The second kind is the one most apps get wrong, and it is invisible unless you test it.

Step 3

Catch errors in async handlers and answer with a 500

In Express 4, an error thrown inside an async route handler is not passed to your error handler; the request hangs or the rejection goes unhandled. Wrap async handlers so that failures call next(err), or move to Express 5, which forwards rejected promises for you. Then add one error-handling middleware with four arguments at the very end that logs the full error, sets res.status(err.status || 500), and sends a short, safe message without the stack trace.

Step 4

Add Next.js error and not-found files

In the App Router, call notFound() from next/navigation whenever data for a page is missing, and create app/not-found.tsx for the page shown. Add app/error.tsx, which must be a client component, for errors inside a route, and app/global-error.tsx for failures in the root layout. In the Pages Router, return { notFound: true } from getServerSideProps or getStaticProps, and add pages/404.tsx and pages/500.tsx. Fetch the data before any streaming starts.

Step 5

Stop unhandled rejections from killing the process

Search the code for promises that are neither awaited inside a try block nor given a .catch(), especially fire-and-forget calls such as sending an email or writing an analytics event after responding. In current Node versions an unhandled rejection ends the process by default. Handle each one where it happens. Add process.on('unhandledRejection') and process.on('uncaughtException') handlers only to log the error and exit cleanly, leaving the restart to your process manager rather than trying to carry on.

Step 6

Run the app under PM2 and survive a reboot

If you host the app yourself, start it with PM2 so a crash is followed by an automatic restart, and check the restart count with pm2 list, where a rising number means crashes are still happening. Run pm2 startup, follow the command it prints, then pm2 save, so the app starts again after the server reboots. A site that returns 502 on every page after a reboot or overnight maintenance nearly always skipped this. Set max_memory_restart to recycle a leaking process.

Step 7

Build the new release before swapping it in

Never run npm install and next build in the folder the live process is serving from. If the build fails half way, the running app can lose files it needs, and a restart finds no production build and exits, producing 500s or 502s. Build each release in its own folder, run it or a smoke test against it, then switch a symlink and use pm2 reload to swap processes one at a time. Keep the previous release so rolling back takes seconds.

Step 8

Keep requests inside platform time limits

On Vercel, Netlify and similar hosts, check the current maximum run time for functions on your plan, and read the function logs for timeout messages next to the URLs the audit flagged. Where your platform allows it, raise the limit for the slow route, for example with export const maxDuration in a Next.js route on Vercel. Better still, cache the result, or move slow work such as report generation to a background job and return quickly. Then rerun the HTTP Error Audit to confirm.

A safe Express error set-up

This pattern covers steps 2 and 3 in one place. The order of the middleware matters: routes first, then the 404 catch-all, then the error handler, which Express recognises because it takes four arguments.

const express = require('express');
const app = express();

// Express 4 only: pass async errors on to the error handler
const wrap = fn => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);

app.get('/products/:slug', wrap(async (req, res) => {
  const product = await db.products.findBySlug(req.params.slug);
  if (!product) return res.status(404).render('not-found');
  res.render('product', { product });
}));

// Anything no route matched
app.use((req, res) => res.status(404).render('not-found'));

// Every error ends up here
app.use((err, req, res, next) => {
  console.error(req.method, req.originalUrl, err);
  if (res.headersSent) return next(err);
  res.status(err.status || 500).render('error');
});

app.listen(3000, '127.0.0.1');

The headersSent check matters when a response was already partly sent, for example while streaming a file. Passing the error on lets Express close the connection rather than trying to send a second set of headers, which would throw another error.

โš ๏ธ Do not send err.message or the stack trace to visitors in production. Messages from database drivers and libraries can include table names, file paths or connection details. Log the detail and show a plain message.

Next.js status codes in practice

Next.js gets most status codes right when you use its own functions, and wrong when you imitate them with ordinary components.

SituationDo thisStatus sent
No route matches the URLProvide app/not-found.tsx or pages/404.tsx404
Route matches but the record is missingCall notFound() (App Router) or return { notFound: true } (Pages Router)404
Rendering a "not found" component yourselfAvoid; replace with notFound()200, a soft 404
Uncaught error while rendering on the serverLet it reach error.tsx; log it500
Page movedpermanentRedirect(), or redirects in next.config.js for fixed paths308
Route handler under app/apiReturn Response.json(body, { status }) with the right codeWhatever you set

With streaming, once Next.js has begun sending the page it cannot change the status. If notFound() is reached inside a part of the page that renders later, inside a Suspense boundary, the response has already gone out as 200; Next.js adds a noindex robots tag in that case, but the code stays wrong. Load the data that decides whether the page exists at the top of the page component, before anything is streamed, so the check runs first.

Static exports built with output: 'export' have no server of their own. The host decides the status, so configure it to serve your generated 404.html with a 404 code for unknown paths rather than serving index.html with 200.

๐Ÿ’ก Middleware runs before every matched request. An error thrown there breaks every page it covers at once, so keep middleware small, wrap its body in try/catch, and limit it with a matcher so it does not run on static files and images.

Reading PM2 when the site shows 502

When a self-hosted Node site suddenly returns 502 on every page, the proxy in front is almost always fine and the Node process is down or restarting. PM2 tells you which within a minute.

pm2 list                  # status, restart count and memory per process
pm2 describe app-name     # script path, working folder, log file locations
pm2 logs app-name --lines 200 --err   # the last errors before each restart

A minimal ecosystem.config.js keeps these settings in the repository rather than in someone's shell history:

module.exports = {
  apps: [{
    name: 'shop-web',
    cwd: '/srv/shop/current',
    script: 'node_modules/next/dist/bin/next',
    args: 'start -p 3000 -H 127.0.0.1',
    instances: 2,
    exec_mode: 'cluster',
    max_memory_restart: '600M',
    env: { NODE_ENV: 'production' }
  }]
};

Hosted platforms and their limits

On managed platforms you do not run PM2 or nginx, but the same failures appear with different names. The platform's function or runtime logs are the equivalent of the server error log, and each platform documents its own error codes.

Preventing repeat failures

Questions and answers

1. Why does my Next.js 404 page return a 200 status?

Usually because the page renders a "not found" component itself instead of calling notFound(), or because the check that decides whether the page exists happens inside a part of the page that streams later, after the 200 has already been sent. Call notFound() from next/navigation as soon as the data lookup fails, near the top of the page component. In the Pages Router, return notFound set to true from getServerSideProps or getStaticProps. Test the result with curl, not the browser.

2. Why does one error take my whole Node site down?

A Node app is one long-running process serving every request. If an exception or promise rejection is not handled anywhere, Node's default behaviour is to log it and exit, and nothing is left listening until the process is started again. The proxy in front then returns 502 for every page. Handle errors in each route, add an error-handling middleware, and run the app under a process manager such as PM2 or systemd so it restarts automatically after any crash.

3. Should I catch uncaughtException and keep the app running?

No. After an uncaught exception the process may be in an unknown state, with half-finished work, open connections or corrupted data in memory. Node's own documentation advises against resuming normal operation after one. Use the handler to log the error with as much detail as possible and then exit, and let PM2, systemd or your platform start a fresh process. Fix the underlying bug by handling that error where it happens, so the handler becomes a rare safety net.

4. How do I stop 502 errors after my server reboots?

The Node app was running in a terminal session or was started by hand and did not come back after the reboot. If you use PM2, run pm2 startup, execute the command it prints to register PM2 as a system service, start your apps, then run pm2 save so the process list is restored at boot. Alternatively, write a systemd unit for the app with a restart policy. Reboot a test server once to confirm the site returns by itself.

5. Why do I get a 500 error only on the live site and not locally?

The most common reasons are a missing or different environment variable in production, a database or API that the live server cannot reach, code paths that only run with production settings, and features that behave differently in the production build, such as caching and static generation. Read the platform or server logs for the failing request; the error message usually names the missing value or failed connection. Compare the live environment variables against your local ones.

6. Does Express 5 change how errors are handled?

Yes, in one important way. In Express 5, if an async route handler or middleware throws or returns a rejected promise, the error is passed to your error-handling middleware automatically. In Express 4 you had to catch it yourself and call next with the error, or use a wrapper function. The error-handling middleware itself still works the same way, with four arguments, placed after all routes. Check the migration notes before upgrading, as other behaviour changed too.

7. Are Vercel function timeouts reported as 504 errors?

Typically yes. When a serverless function runs longer than the allowed duration, the platform stops it and returns a gateway timeout, and the function logs show a timeout message for that request. The allowed duration depends on your plan and settings and has changed over time, so check the current documentation. You can raise it for a specific route within your plan's limits, but caching the result or moving slow work to a background job is usually the better fix.

Check every status code your app sends

The HTTP Error Audit requests every URL in your crawl scope, records the status code and response time, follows redirects and flags anything outside the 200 range, so crashes and wrong codes show up before search engines find them.

Open HTTP Error Fixes โ†’
๐Ÿ’ฌ Got a problem?
Related Guides: All HTTP Error Fixes  ·  502 Bad Gateway Errors  ·  Site Audit Fixes for Next.js  ·  500 Internal Server Errors  ·  Learning Hub

About aiwebpageseo

aiwebpageseo.com is a data-driven SEO and AEO (Answer Engine Optimisation) platform providing a free suite of technical SEO tools. Rather than relying on AI-theorised assumptions, the platform analyses live URL performance, delivering objective diagnostics, page speed metrics, CLS debugging, and site crawl data alongside actionable technical tutorials.