Skip to content

Fix: Next.js 500 Internal Server Error

FixDevs ·

Quick Answer

How to fix the Next.js 500 Internal Server Error by checking server logs, fixing getServerSideProps, API routes, environment variables, database connections, middleware, and deployment issues.

The Error

You open your Next.js application and see:

500 - Internal Server Error

Or in the browser console:

GET http://localhost:3000/ 500 (Internal Server Error)

Sometimes the error appears only on specific pages. Other times it hits every route. In production, you might see a generic “Internal Server Error” page with zero useful details.

The 500 status code means something went wrong on the server side. Next.js caught an unhandled exception and could not render the page. The fix depends on what triggered it — and the browser will almost never tell you.

Why This Happens

A 500 error in Next.js is a catch-all for any unhandled server-side failure. The most common causes:

  • Unhandled exceptions in getServerSideProps, getStaticProps, or Server Components that crash during rendering.
  • API route errors where your handler throws or fails to send a response.
  • Missing or misconfigured environment variables that your server-side code depends on.
  • Database or external API connection failures that throw errors without proper error handling.
  • Middleware crashes that block requests before they reach your page.
  • Build-time vs runtime mismatches where code works in next dev but breaks in next build or next start.
  • Deployment configuration issues like wrong Node.js versions, missing dependencies, or platform-specific quirks.

The key insight: the browser only shows “500 Internal Server Error.” The actual error message lives in your server logs. Always start there.

Fix 1: Check Server Logs (Terminal, Not Browser)

The browser gives you nothing useful for a 500 error. The real error is in your terminal or server logs.

In development, check the terminal where you ran next dev:

npm run dev

The stack trace prints directly to the terminal. Look for lines like:

Error: Cannot read properties of undefined (reading 'map')
    at getServerSideProps (/app/pages/dashboard.js:15:22)

In production, check your hosting platform’s logs:

  • Vercel: Go to your project dashboard, click Deployments, select the deployment, then click Runtime Logs or Functions.
  • Docker: Run docker logs <container_name>.
  • Self-hosted: Check your process manager logs (PM2: pm2 logs, systemd: journalctl -u your-app).

If you are running next start directly:

NODE_ENV=production next start 2>&1 | tee app.log

This pipes both stdout and stderr to a log file while still printing to the terminal.

Pro Tip: In development, Next.js shows a detailed error overlay in the browser for client-side errors. But server-side errors (like those in getServerSideProps) only appear in the terminal. If you are staring at the browser waiting for details, you are looking in the wrong place.

Once you find the actual error message, match it to one of the fixes below.

Fix 2: Fix getServerSideProps / getStaticProps Errors

The most common source of 500 errors. Any unhandled exception in these functions crashes the page.

Problem: Accessing properties on undefined data.

// pages/dashboard.js
export async function getServerSideProps() {
  const res = await fetch('https://api.example.com/user');
  const data = await res.json();

  return {
    props: {
      userName: data.user.name, // Crashes if data.user is undefined
    },
  };
}

Fix: Add null checks and error handling.

export async function getServerSideProps() {
  try {
    const res = await fetch('https://api.example.com/user');

    if (!res.ok) {
      return { notFound: true };
    }

    const data = await res.json();

    return {
      props: {
        userName: data?.user?.name ?? 'Unknown',
      },
    };
  } catch (error) {
    console.error('Failed to fetch user:', error);
    return { notFound: true };
  }
}

Returning { notFound: true } shows your 404 page instead of a 500 error. You can also redirect:

return {
  redirect: {
    destination: '/error',
    permanent: false,
  },
};

Another common mistake: returning non-serializable data.

export async function getServerSideProps() {
  const date = new Date();

  return {
    props: {
      // Date objects are not serializable -- this causes a 500
      createdAt: date,
    },
  };
}

Fix it by converting to a string or timestamp:

return {
  props: {
    createdAt: date.toISOString(),
  },
};

For the App Router equivalent, the same principles apply to Server Components and generateStaticParams. If your server component throws, the whole page returns a 500. Wrap risky operations in try/catch and handle failures gracefully. For related hydration issues that can mask these errors, see Next.js hydration failed.

Fix 3: Fix API Route Errors

API routes are another frequent 500 source. Two main patterns cause it.

Problem 1: Uncaught exceptions.

// pages/api/users.js
export default async function handler(req, res) {
  const users = await db.query('SELECT * FROM users');
  res.json(users); // If db.query throws, the 500 is unhandled
}

Fix: Wrap in try/catch.

export default async function handler(req, res) {
  try {
    const users = await db.query('SELECT * FROM users');
    res.status(200).json(users);
  } catch (error) {
    console.error('Database query failed:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
}

Problem 2: Not sending a response. If your handler finishes without calling res.json(), res.send(), or res.end(), the request hangs and eventually times out — or Next.js returns a 500.

export default function handler(req, res) {
  if (req.method === 'POST') {
    // handle POST
    res.status(201).json({ success: true });
  }
  // GET requests fall through with no response -- 500
}

Fix: Always send a response for every code path.

export default function handler(req, res) {
  if (req.method === 'POST') {
    res.status(201).json({ success: true });
    return;
  }

  res.status(405).json({ error: 'Method not allowed' });
}

For App Router route handlers (app/api/*/route.ts), the same rules apply but the syntax differs. You must return a Response object. For a deeper dive, see Next.js API route not working.

Fix 4: Fix Environment Variable Issues

Missing environment variables are a silent killer. Your code runs fine in development with a .env.local file but explodes in production.

The NEXT_PUBLIC_ prefix rule:

  • Variables without NEXT_PUBLIC_ are only available server-side (getServerSideProps, API routes, Server Components).
  • Variables with NEXT_PUBLIC_ are inlined into the client bundle at build time.

Problem: Using a server-only variable on the client.

// This is undefined on the client
const apiKey = process.env.API_SECRET_KEY;

Problem: Forgetting to set variables in production.

Your .env.local file is not deployed. You must set environment variables in your hosting platform:

  • Vercel: Project Settings > Environment Variables.
  • Docker: Pass them via -e flags or a .env file in docker run.
  • Self-hosted: Set them in your process manager or shell profile.

Fix: Validate environment variables at startup.

// lib/env.js
const requiredEnvVars = [
  'DATABASE_URL',
  'API_SECRET_KEY',
  'NEXT_PUBLIC_SITE_URL',
];

for (const envVar of requiredEnvVars) {
  if (!process.env[envVar]) {
    throw new Error(`Missing required environment variable: ${envVar}`);
  }
}

Import this file early in your application (e.g., in next.config.js or a top-level layout) so you get a clear error message instead of a cryptic 500.

Common Mistake: You add a new environment variable, deploy, and get a 500. You check the code — it looks fine. The problem: you added the variable to .env.local but forgot to add it to your production environment. Always update both simultaneously.

Fix 5: Fix Database or External API Connection Failures

Your application depends on a database or third-party API. When that connection fails, you get a 500.

Common causes:

  • Database server is down or unreachable from your deployment environment.
  • Connection string is wrong (typo, wrong port, wrong credentials).
  • Connection pool is exhausted (too many open connections).
  • Network firewall blocks the connection in production but allows it locally.

Fix: Add connection error handling and retries.

import { Pool } from 'pg';

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 10,
  connectionTimeoutMillis: 5000,
});

export async function query(text, params) {
  try {
    const result = await pool.query(text, params);
    return result.rows;
  } catch (error) {
    console.error('Database query error:', error.message);
    throw error; // Re-throw so the caller can handle it
  }
}

For external APIs, add timeout and retry logic:

async function fetchWithRetry(url, options = {}, retries = 3) {
  for (let i = 0; i < retries; i++) {
    try {
      const controller = new AbortController();
      const timeout = setTimeout(() => controller.abort(), 5000);

      const res = await fetch(url, {
        ...options,
        signal: controller.signal,
      });

      clearTimeout(timeout);

      if (!res.ok) {
        throw new Error(`HTTP ${res.status}`);
      }

      return await res.json();
    } catch (error) {
      if (i === retries - 1) throw error;
      await new Promise((r) => setTimeout(r, 1000 * (i + 1)));
    }
  }
}

In serverless environments (Vercel, AWS Lambda), be mindful of connection pooling. Each invocation may create a new database connection. Use connection pooling solutions like PgBouncer or Prisma’s connection pool to avoid exhausting your database.

Fix 6: Fix Middleware Errors

Next.js middleware (middleware.ts or middleware.js at the project root) runs before every matched request. If it throws, every page returns a 500.

Problem: Middleware crashes on certain requests.

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const token = request.cookies.get('auth-token')?.value;
  const decoded = JSON.parse(atob(token)); // Crashes if token is missing or malformed
  // ...
}

Fix: Add defensive checks.

export function middleware(request: NextRequest) {
  const token = request.cookies.get('auth-token')?.value;

  if (!token) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  try {
    const decoded = JSON.parse(atob(token));
    // Validate decoded token
  } catch {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  return NextResponse.next();
}

Limit middleware scope using the matcher config to avoid running middleware on routes that do not need it:

export const config = {
  matcher: ['/dashboard/:path*', '/api/protected/:path*'],
};

If you recently added or modified middleware and suddenly all pages return 500, the middleware file is the first place to check. A common issue is importing Node.js modules (like fs or path) in middleware — these are not available in the Edge Runtime. For more on module resolution problems, see Next.js module not found: Can’t resolve ‘fs’.

Fix 7: Fix Build vs Runtime Errors

Code that works with next dev can break with next build and next start. The development server is more forgiving.

Run a production build locally to catch issues:

next build && next start

Common differences between dev and production:

  1. Static pages are pre-rendered at build time. If getStaticProps fails during next build, the build itself fails. But if it fails during ISR (Incremental Static Regeneration) at runtime, you get a 500.

  2. TypeScript errors are ignored in dev but can cause build failures. Run tsc --noEmit to check for type errors.

  3. Missing dependencies. Dev might use cached modules that are not in package.json. A clean install (rm -rf node_modules && npm install) catches this.

  4. Dynamic imports behave differently. Code splitting works differently in production. If a dynamically imported module fails to load:

const Chart = dynamic(() => import('../components/Chart'), {
  loading: () => <p>Loading...</p>,
  ssr: false, // Disable server-side rendering for client-only libraries
});
  1. Image optimization issues. The default image optimizer requires sharp in production. If it is missing or the wrong version, image-heavy pages may 500. See Next.js image optimization error for details.

Fix: Always test with a production build before deploying.

npm run build && npm run start

Add this to your CI/CD pipeline so broken builds never reach production.

Fix 8: Fix Deployment-Specific Issues

The 500 only happens in production. Everything works locally. Here are the platform-specific fixes.

Vercel

  • Check function logs in the Vercel dashboard under Deployments > Functions.
  • Serverless function timeout: Free tier has a 10-second limit. Long database queries or API calls will time out and return a 500. Optimize your queries or upgrade your plan.
  • Memory limits: Functions default to 1024 MB. Large data processing can exceed this. Set it in vercel.json:
{
  "functions": {
    "api/*.js": {
      "memory": 3008
    }
  }
}
  • Region mismatch: If your database is in us-east-1 but your Vercel functions deploy to iad1 (EU), latency might cause timeouts. Match your function region to your database region.

Docker

  • Node.js version mismatch: Make sure your Dockerfile uses the same Node.js version as your local environment.
FROM node:20-alpine AS base
  • Missing build dependencies: Some npm packages require native build tools. Add them to your Dockerfile:
RUN apk add --no-cache libc6-compat
  • File system permissions: If your app writes to the file system (e.g., file uploads), ensure the container user has write permissions.

Node.js Version

Next.js 14+ requires Node.js 18.17 or later. Older Node.js versions cause cryptic 500 errors.

Check your version:

node --version

If you are on an older version, update Node.js. Use a .nvmrc file to pin the version:

20

Then run:

nvm use

Self-Hosted (PM2, systemd)

  • Ensure next build ran before next start. Running next start without building first causes immediate 500 errors.
  • Set NODE_ENV=production explicitly. Some libraries behave differently without it.
  • Check file permissions. The .next build output directory must be readable by the user running the process.

Still Not Working?

If none of the fixes above solve your 500 error, try these approaches.

Custom Error Pages

Create a custom 500 page to show useful debugging info in development:

// pages/500.js (Pages Router)
export default function Custom500() {
  return <h1>500 - Server Error</h1>;
}

For the App Router:

// app/error.tsx
'use client';

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  return (
    <div>
      <h2>Something went wrong</h2>
      <p>{error.message}</p>
      <button onClick={reset}>Try again</button>
    </div>
  );
}

Error Boundary Debugging

React error boundaries catch rendering errors. Add one to isolate which component is crashing:

// components/ErrorBoundary.jsx
import React from 'react';

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }

  componentDidCatch(error, errorInfo) {
    console.error('ErrorBoundary caught:', error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return <p>Component error: {this.state.error?.message}</p>;
    }
    return this.props.children;
  }
}

Wrap suspicious components individually to find the one causing the 500:

<ErrorBoundary>
  <SuspiciousComponent />
</ErrorBoundary>

For related rendering issues, check React: Objects are not valid as a React child.

App Router vs Pages Router Differences

If you are using Next.js 13+ with the App Router, error handling works differently:

  • Server Components do not support try/catch in JSX. Use error.tsx files to catch errors at the route segment level.
  • loading.tsx files handle suspense boundaries. A missing loading state can cause streaming errors that manifest as 500s.
  • route.ts files (App Router API routes) must return Response objects, not use res.json().
  • Server Actions that throw unhandled errors return 500s. Always wrap them in try/catch.
// app/actions.ts
'use server';

export async function submitForm(formData: FormData) {
  try {
    // process form
  } catch (error) {
    // Return an error state instead of throwing
    return { error: 'Submission failed' };
  }
}

If you recently migrated from Pages Router to App Router, double-check that you are not mixing paradigms. Having both pages/ and app/ directories for the same route causes conflicts and unpredictable 500 errors.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles