---
title: "Server Architecture"
description: "The Next.js server architecture provides the foundational runtime environment, HTTP request processing pipeline, and modular routing infrastructure that drives both development and production serve..."
last_updated: "2026-10-12T06:38:56.881153+00:00"
canonical_url: "https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/server-runtime/server-architecture"
---

<details>
<summary>Relevant source files</summary>

The following files were used as context for generating this wiki page:

- [packages/next/src/server/next-server.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/next-server.ts)
- [packages/next/src/server/base-server.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/base-server.ts)
- [packages/next/src/server/next.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/next.ts)
- [packages/next/src/server/lib/router-server.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/lib/router-server.ts)
- [packages/next/src/server/lib/start-server.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/lib/start-server.ts)
- [packages/next/src/server/dev/hot-reloader-turbopack.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/dev/hot-reloader-turbopack.ts)
- [packages/next/src/server/dev/next-dev-server.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/dev/next-dev-server.ts)
- [packages/next/src/server/base-http/index.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/base-http/index.ts)
- [packages/next/src/server/base-http/node.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/base-http/node.ts)
- [packages/next/src/server/web/spec-extension/adapters/next-request.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/web/spec-extension/adapters/next-request.ts)
- [packages/next/src/server/node-environment-baseline.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/node-environment-baseline.ts)
- [packages/next/server.js](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/server.js)
- [packages/next/src/server/app-render/entry-base.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/app-render/entry-base.ts)
- [packages/next/src/api/server.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/api/server.ts)
- [packages/next/src/lib/is-error.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/lib/is-error.ts)
- [packages/next/src/server/request-meta.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/request-meta.ts)
</details>

## Overview

The Next.js server architecture provides the foundational runtime environment, HTTP request processing pipeline, and modular routing infrastructure that drives both development and production servers. It handles programmatic instantiation via wrapper classes, orchestrates process socket binding and listener bootstrapping, and implements abstract base server contracts specialized for Node.js execution. By normalizing low-level Node.js streams into Web standard request and response abstractions, the server architecture unifies handling across disparate runtimes while managing robust error detection, bubbling, and request metadata propagation throughout the rendering lifecycle.

Sources: [packages/next/src/server/next.ts:179-190](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/next.ts#L179-L190), [packages/next/src/server/lib/router-server.ts:131-145](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/lib/router-server.ts#L131-L145), [packages/next/src/server/base-server.ts:477-492](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/base-server.ts#L477-L492), [packages/next/src/server/next-server.ts:198-202](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/next-server.ts#L198-L202), [packages/next/src/server/base-http/node.ts:19-27](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/base-http/node.ts#L19-L27), [packages/next/src/server/request-meta.ts:51-110](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/request-meta.ts#L51-L110)

## Public Server Entrypoints

### Wrapper Server and Lifecycle Configuration

The `NextServer` wrapper class implements `NextWrapperServer` to serve as the programmatic public interface for custom servers and commands like `next start`. It manages asynchronous configuration loading via `loadConfig`, maps runtime phases to `PHASE_DEVELOPMENT_SERVER` or `PHASE_PRODUCTION_SERVER`, and initializes either `NextNodeServer` or `NextDevServer` depending on `options.dev`.

```typescript
export class NextServer implements NextWrapperServer {
  private serverPromise?: Promise<NextNodeServer>
  private server?: NextNodeServer
  private reqHandler?: NodeRequestHandler
  private reqHandlerPromise?: Promise<NodeRequestHandler>
  private preparedAssetPrefix?: string

  public options: NextServerOptions

  constructor(options: NextServerOptions) {
    this.options = options
  }
```
Sources: [packages/next/src/server/next.ts:179-190](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/next.ts#L179-L190)

```typescript
private async [SYMBOL_LOAD_CONFIG]() {
  const dir = path.resolve(
    /* turbopackIgnore: true */ this.options.dir || '.'
  )

  const config = await loadConfig(
    this.options.dev ? PHASE_DEVELOPMENT_SERVER : PHASE_PRODUCTION_SERVER,
    dir,
    {
      customConfig: this.options.conf,
      silent: true,
    }
  )

```
Sources: [packages/next/src/server/next.ts:342-355](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/next.ts#L342-L355)

> [!WARNING]
> `NextServer` enforces strict checks on production output configurations during initialization. If `conf.output === 'standalone'` is set without `__NEXT_PRIVATE_STANDALONE_CONFIG`, a warning is logged, whereas `conf.output === 'export'` throws an immediate error instructing the user to use `serve` instead.

```typescript
if (!this.options.dev) {
  if (conf.output === 'standalone') {
    if (!process.env.__NEXT_PRIVATE_STANDALONE_CONFIG) {
      log.warn(
        `"next start" does not work with "output: standalone" configuration. Use "node .next/standalone/server.js" instead.`
      )
    }
  } else if (conf.output === 'export') {
    throw new Error(
      `"next start" does not work with "output: export" configuration. Use "npx serve@latest out" instead.`
    )
  }
}
```
Sources: [packages/next/src/server/next.ts:389-401](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/next.ts#L389-L401)

### Deprecated Custom Server Methods

Custom server instances expose several legacy methods that are guarded with runtime warnings via `warnDeprecatedCustomServerMethod`. The mapping of deprecated methods to alternative guidance is maintained in `DEPRECATED_CUSTOM_SERVER_METHOD_GUIDANCE`.

| Deprecated Method | Guidance Message |
| :--- | :--- |
| `setAssetPrefix` | Please configure `assetPrefix` in `next.config.js` instead. |
| `logError` | Please use application logging instead. |
| `logErrorWithOriginalStack` | Please use application logging instead. |
| `revalidate` | Please use documented application revalidation APIs instead. |
| `render` | Please use `app.getRequestHandler()` with an adjusted parsed URL instead. |
| `renderToHTML` | Please use `app.getRequestHandler()` with an adjusted parsed URL instead. |
| `renderError` | Please use `app.getRequestHandler()` with an adjusted parsed URL instead. |
| `renderErrorToHTML` | Please use `app.getRequestHandler()` with an adjusted parsed URL instead. |
| `render404` | Please use `app.getRequestHandler()` with an adjusted parsed URL instead. |

```typescript
const DEPRECATED_CUSTOM_SERVER_METHOD_GUIDANCE: Record<
  DeprecatedCustomServerMethod,
  string
> = {
  setAssetPrefix: 'Please configure `assetPrefix` in `next.config.js` instead.',
  logError: 'Please use application logging instead.',
  logErrorWithOriginalStack: 'Please use application logging instead.',
  revalidate: 'Please use documented application revalidation APIs instead.',
  render:
    'Please use `app.getRequestHandler()` with an adjusted parsed URL instead.',
  renderToHTML:
    'Please use `app.getRequestHandler()` with an adjusted parsed URL instead.',
  renderError:
    'Please use `app.getRequestHandler()` with an adjusted parsed URL instead.',
  renderErrorToHTML:
    'Please use `app.getRequestHandler()` with an adjusted parsed URL instead.',
  render404:
    'Please use `app.getRequestHandler()` with an adjusted parsed URL instead.',
}
```
Sources: [packages/next/src/server/next.ts:84-102](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/next.ts#L84-L102)

## Server Initialization and Bootstrapping

### Baseline Runtime Environment and Server Startup

Before loading any application modules or establishing network sockets, the server establishes its baseline runtime environment via `node-environment-baseline.ts`. This initialization step ensures that `AsyncLocalStorage` from `async_hooks` is exposed on `globalThis` for React context propagation if the host environment does not provide it natively. Additionally, it defines a getter property on `globalThis` for `WebSocket`, lazily requiring `next/dist/compiled/ws` when accessed.

```typescript
if (typeof (globalThis as any).AsyncLocalStorage !== 'function') {
  const { AsyncLocalStorage } =
    require('async_hooks') as typeof import('async_hooks')
  ;(globalThis as any).AsyncLocalStorage = AsyncLocalStorage
}

if (typeof (globalThis as any).WebSocket !== 'function') {
  Object.defineProperty(globalThis, 'WebSocket', {
    configurable: true,
    get() {
      return (
        require('next/dist/compiled/ws') as typeof import('next/dist/compiled/ws')
      ).WebSocket
    },
    set(value) {
      Object.defineProperty(globalThis, 'WebSocket', {
        configurable: true,
        writable: true,
        value,
      })
    },
  })
```
Sources: [packages/next/src/server/node-environment-baseline.ts:5-26](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/node-environment-baseline.ts#L5-L26)

The core bootstrapping function `startServer` in `start-server.ts` updates `process.title` to `next-server (v${process.env.__NEXT_VERSION})` and instantiates the underlying HTTP listener. Depending on whether `selfSignedCertificate` options and production constraints are met, it invokes either `https.createServer` or `http.createServer` with a `requestListener`.

```typescript
let { port } = serverOptions

process.title = `next-server (v${process.env.__NEXT_VERSION})`
let handlersReady = () => {}
let handlersError = () => {}

let handlersPromise: Promise<void> | undefined = new Promise<void>(
  (resolve, reject) => {
    handlersReady = resolve
    handlersError = reject
  }
)
let requestHandler: WorkerRequestHandler = async (
  req: IncomingMessage,
  res: ServerResponse
): Promise<void> => {
  if (handlersPromise) {
    await handlersPromise
    return requestHandler(req, res)
  }
  throw new Error('Invariant request handler was not setup')
}
let upgradeHandler: WorkerUpgradeHandler = async (
  req,
  socket,
  head
): Promise<void> => {
  if (handlersPromise) {
    await handlersPromise
    return upgradeHandler(req, socket, head)
  }
  throw new Error('Invariant upgrade handler was not setup')
}

let nextServer: NextServer | undefined
let devMemoryThresholdRestart = true

// setup server listener as fast as possible
if (selfSignedCertificate && !isDev) {
  throw new Error(
    'Using a self signed certificate is only supported with `next dev`.'
  )
}

async function requestListener(req: IncomingMessage, res: ServerResponse) {
  try {
    if (handlersPromise) {
      await handlersPromise
      handlersPromise = undefined
    }
    await requestHandler(req, res)
  } catch (err) {
    res.statusCode = 500
    res.end('Internal Server Error')
    Log.error(`Failed to handle request for ${req.url}`)
    console.error(err)
  } finally {
    const memoryRestartStats = getMemoryRestartStats(
      isDev,
      devMemoryThresholdRestart,
      v8.getHeapStatistics
    )
    if (memoryRestartStats) {
      Log.warn(
        `Server is approaching the used memory threshold, restarting...`
      )
      trace('server-restart-close-to-memory-threshold', undefined, {
        'memory.heapSizeLimit': String(memoryRestartStats.heap_size_limit),
        'memory.heapUsed': String(memoryRestartStats.used_heap_size),
      }).stop()
      flushAllTraces()
      process.exit(RESTART_EXIT_CODE)
    }
  }
}

const server = selfSignedCertificate
  ? https.createServer(
      {
        key: fs.readFileSync(selfSignedCertificate.key),
```
Sources: [packages/next/src/server/lib/start-server.ts:204-288](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/lib/start-server.ts#L204-L288)

> [!CAUTION]
> Using a self-signed certificate via `selfSignedCertificate` is strictly restricted to development mode (`isDev`). Attempting to use a self-signed certificate when `isDev` is false causes `startServer` to throw an immediate error during listener setup.

```typescript
if (selfSignedCertificate && !isDev) {
  throw new Error(
    'Using a self signed certificate is only supported with `next dev`.'
  )
}
```
Sources: [packages/next/src/server/lib/start-server.ts:242-246](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/lib/start-server.ts#L242-L246)

### Socket Binding, Port Allocation, and Termination Signals

Once the server instance is configured with optional `keepAliveTimeout` and upgrade event handlers, it binds to the specified port and hostname. If `allowRetry` is enabled and an `EADDRINUSE` error occurs on `error`, the [development server](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/development/development-server) automatically increments the port by one and retries up to 10 times before logging a fatal startup failure and exiting the process with code `1`.

```typescript
if (keepAliveTimeout) {
  server.keepAliveTimeout = keepAliveTimeout
}
server.on('upgrade', async (req, socket, head) => {
  try {
    await upgradeHandler(req, socket, head)
  } catch (err) {
    socket.destroy()
    Log.error(`Failed to handle request for ${req.url}`)
    console.error(err)
  }
})

let portRetryCount = 0
const originalPort = port

server.on('error', (err: NodeJS.ErrnoException) => {
  if (
    allowRetry &&
    port &&
    isDev &&
    err.code === 'EADDRINUSE' &&
    portRetryCount < 10
  ) {
    port += 1
    portRetryCount += 1
    server.listen(port, hostname)
  } else {
    Log.error(`Failed to start server`)
    console.error(err)
    process.exit(1)
  }
})
```
Sources: [packages/next/src/server/lib/start-server.ts:290-322](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/lib/start-server.ts#L290-L322)

Upon successfully emitting the `listening` event, the server registers signal handlers for graceful process termination unless `NEXT_MANUAL_SIG_HANDLE` is set in the environment.

| Signal | Exit Code Behavior |
| :--- | :--- |
| `SIGINT` | Exits process with code `130` (signal termination) |
| `SIGTERM` | Exits process with code `143` (signal termination) |
| Default / Unhandled | Exits process with code `128` |

```typescript
    switch (signal) {
      case 'SIGINT':
        process.exit(130)
        break
      case 'SIGTERM':
        process.exit(143)
        break
      default:
        // Make sure all handled signals have explicit exit codes.
        // This is just a fallback to guard against unsound types.
        signal satisfies never
        process.exit(128)
    }
  })()
}

// Make sure commands gracefully respect termination signals (e.g. from Docker)
// Allow the graceful termination to be manually configurable
if (!process.env.NEXT_MANUAL_SIG_HANDLE) {
  process.on('SIGINT', cleanup)
  process.on('SIGTERM', cleanup)
}
```
Sources: [packages/next/src/server/lib/start-server.ts:471-492](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/lib/start-server.ts#L471-L492)

## BaseServer Architecture and Core Contracts

### Abstract Server Responsibilities and Constructor Initialization

`BaseServer` serves as the abstract coordinator for Next.js HTTP request handling, environment configuration, and asset normalization across different execution runtimes. The constructor initializes configuration values, sets up path resolvers, and instantiates request normalizers based on whether app directories, pages directories, or internationalization settings are active.

```typescript
public constructor(options: ServerOptions) {
  const {
    dir = '.',
    quiet = false,
    conf,
    dev = false,
    minimalMode = false,
    hostname,
    port,
    experimentalTestProxy,
  } = options

  this.dev = dev
  this.experimentalTestProxy = experimentalTestProxy
  this.serverOptions = options

  this.dir = path.resolve(/* turbopackIgnore: true */ dir)

  this.quiet = quiet
  this.loadEnvConfig({ dev, forceReload: false })

  // TODO: should conf be normalized to prevent missing
  // values from causing issues as this can be user provided
  this.nextConfig = conf as NextConfigRuntime
  if (
    (dev || process.env.__NEXT_DEV_SERVER) &&
    this.nextConfig.experimental.requestInsights
  ) {
    process.env.__NEXT_REQUEST_INSIGHTS = 'true'
  }

  if (this.nextConfig.experimental.runtimeServerDeploymentId) {
    if (!process.env.NEXT_DEPLOYMENT_ID) {
      throw new Error(
        'process.env.NEXT_DEPLOYMENT_ID is missing but runtimeServerDeploymentId is enabled'
      )
    }
    this.deploymentId = process.env.NEXT_DEPLOYMENT_ID
  } else {
    let id = this.nextConfig.experimental.useSkewCookie
      ? ''
      : this.nextConfig.deploymentId || ''

    this.deploymentId = id
    process.env.NEXT_DEPLOYMENT_ID = id
  }
  ;(globalThis as any).NEXT_CLIENT_ASSET_SUFFIX =
    this.nextConfig.supportsImmutableAssets || !this.deploymentId
      ? ''
      : `?dpl=${this.deploymentId}`

  this.hostname = hostname
  if (this.hostname) {
    // we format the hostname so that it can be fetched
    this.fetchHostname = formatHostname(this.hostname)
  }
  this.port = port
  this.distDir = path.join(
    /* turbopackIgnore: true */ this.dir,
    this.nextConfig.distDir
  )
  this.pagesNormalizers = new PagesNormalizers(this.distDir)
  this.appNormalizers = new AppNormalizers(this.distDir)
  this.publicDir = this.getPublicDir()
  this.hasStaticDir = !minimalMode && this.getHasStaticDir()

  this.i18nProvider = this.nextConfig.i18n?.locales
    ? new I18NProvider(this.nextConfig.i18n)
    : undefined

  // Configure the locale normalizer, it's used for routes inside `pages/`.
  this.localeNormalizer = this.i18nProvider
    ? new LocaleRouteNormalizer(this.i18nProvider)
    : undefined

  const { assetPrefix, generateEtags } = this.nextConfig

  this.buildId = this.getBuildId()
  // this is a hack to avoid Webpack knowing this is equal to this.minimalMode
  // because we replace this.minimalMode to true in production bundles.
```
Sources: [packages/next/src/server/base-server.ts:477-580](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/base-server.ts#L477-L580)

During initialization, the server resolves the root application directory (`dir`), configures deployment identifiers via `process.env.NEXT_DEPLOYMENT_ID` or `runtimeServerDeploymentId`, and sets up locale normalizers if internationalization is enabled. It configures path normalizers for Pages (`PagesNormalizers`) and App (`AppNormalizers`) routes within the distribution directory (`distDir`).

```typescript
this.dir = path.resolve(/* turbopackIgnore: true */ dir)

this.quiet = quiet
this.loadEnvConfig({ dev, forceReload: false })

// TODO: should conf be normalized to prevent missing
// values from causing issues as this can be user provided
this.nextConfig = conf as NextConfigRuntime
if (
  (dev || process.env.__NEXT_DEV_SERVER) &&
  this.nextConfig.experimental.requestInsights
) {
  process.env.__NEXT_REQUEST_INSIGHTS = 'true'
}

if (this.nextConfig.experimental.runtimeServerDeploymentId) {
  if (!process.env.NEXT_DEPLOYMENT_ID) {
    throw new Error(
      'process.env.NEXT_DEPLOYMENT_ID is missing but runtimeServerDeploymentId is enabled'
    )
  }
  this.deploymentId = process.env.NEXT_DEPLOYMENT_ID
} else {
  let id = this.nextConfig.experimental.useSkewCookie
    ? ''
    : this.nextConfig.deploymentId || ''

  this.deploymentId = id
  process.env.NEXT_DEPLOYMENT_ID = id
}
;(globalThis as any).NEXT_CLIENT_ASSET_SUFFIX =
  this.nextConfig.supportsImmutableAssets || !this.deploymentId
    ? ''
    : `?dpl=${this.deploymentId}`

this.hostname = hostname
if (this.hostname) {
  // we format the hostname so that it can be fetched
  this.fetchHostname = formatHostname(this.hostname)
}
this.port = port
this.distDir = path.join(
  /* turbopackIgnore: true */ this.dir,
  this.nextConfig.distDir
)
this.pagesNormalizers = new PagesNormalizers(this.distDir)
this.appNormalizers = new AppNormalizers(this.distDir)
this.publicDir = this.getPublicDir()
this.hasStaticDir = !minimalMode && this.getHasStaticDir()
```
Sources: [packages/next/src/server/base-server.ts:493-541](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/base-server.ts#L493-L541)

> [!CAUTION]
> If `runtimeServerDeploymentId` is enabled in `nextConfig.experimental`, `process.env.NEXT_DEPLOYMENT_ID` must be defined beforehand. If the environment variable is missing during server startup, `BaseServer` throws an immediate error.

```typescript
if (this.nextConfig.experimental.runtimeServerDeploymentId) {
  if (!process.env.NEXT_DEPLOYMENT_ID) {
    throw new Error(
      'process.env.NEXT_DEPLOYMENT_ID is missing but runtimeServerDeploymentId is enabled'
    )
  }
  this.deploymentId = process.env.NEXT_DEPLOYMENT_ID
```
Sources: [packages/next/src/server/base-server.ts:508-514](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/base-server.ts#L508-L514)

### Normalizers and Render Options Configuration

The constructor constructs a core set of request path normalizers and builds the internal `renderOpts` object passed down to rendering pipelines. The normalizers object routes incoming requests through specialized prefix checkers depending on whether the server operates in minimal mode or handles server components.

| Normalizer Property | Class Type / Condition |
| :--- | :--- |
| `rsc` | `RSCPathnameNormalizer` (Active if `enabledDirectories.app` and `minimalMode` are true) |
| `segmentPrefetchRSC` | `SegmentPrefixRSCPathnameNormalizer` (Active if `minimalMode` is true) |
| `data` | `NextDataPathnameNormalizer` (Active if `enabledDirectories.pages` is true, initialized with `buildId`) |

```typescript
this.normalizers = {
  // We should normalize the pathname from the RSC prefix only in minimal
  // mode as otherwise that route is not exposed external to the server as
  // we instead only rely on the headers.
  rsc:
    this.enabledDirectories.app && this.minimalMode
      ? new RSCPathnameNormalizer()
      : undefined,
  segmentPrefetchRSC: this.minimalMode
    ? new SegmentPrefixRSCPathnameNormalizer()
    : undefined,
  data: this.enabledDirectories.pages
    ? new NextDataPathnameNormalizer(this.buildId)
    : undefined,
}
```
Sources: [packages/next/src/server/base-server.ts:566-580](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/base-server.ts#L566-L580)

The server also compiles `renderOpts` using configuration options extracted from `this.nextConfig`, including base paths, asset prefixes, experimental feature flags like taint support, script workers, optimize CSS, and internationalized domain settings.

```typescript
this.renderOpts = {
  dir: this.dir,
  supportsDynamicResponse: true,
  trailingSlash: this.nextConfig.trailingSlash,
  poweredByHeader: this.nextConfig.poweredByHeader,
  generateEtags,
  previewProps: this.getPreviewProps(),
  basePath: this.nextConfig.basePath,
  images: this.nextConfig.images,
  optimizeCss: this.nextConfig.experimental.optimizeCss,
  nextConfigOutput: this.nextConfig.output,
  nextScriptWorkers: this.nextConfig.experimental.nextScriptWorkers,
  disableOptimizedLoading:
    this.nextConfig.experimental.disableOptimizedLoading,
  domainLocales: this.nextConfig.i18n?.domains,
  distDir: this.distDir,
  serverComponents: this.enabledDirectories.app,
  cacheLifeProfiles: this.nextConfig.cacheLife,
  staticPageGenerationTimeout: this.nextConfig.staticPageGenerationTimeout,
  enableTainting: this.nextConfig.experimental.taint,
  crossOrigin: this.nextConfig.crossOrigin
    ? this.nextConfig.crossOrigin
    : undefined,
  largePageDataBytes: this.nextConfig.experimental.largePageDataBytes,

  isExperimentalCompile: options.compileMode
    ? options.compileMode.isExperimentalCompile
    : this.nextConfig.experimental.isExperimentalCompile,
```
Sources: [packages/next/src/server/base-server.ts:584-611](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/base-server.ts#L584-L611)

## NextNodeServer Specialization and Execution

### Node.js Platform Server Implementation and Request Normalization

`NextNodeServer` extends `BaseServer` to implement the Node.js production server runtime, inheriting platform-specific request routing, file system caching bindings, and custom server handlers. The server initializes request adapters that bridge incoming Node.js `IncomingMessage` and `ServerResponse` streams into `NodeNextRequest` and `NodeNextResponse` wrappers.

```typescript
protected normalizeReq(
  req: NodeNextRequest | IncomingMessage
): NodeNextRequest {
  return !(req instanceof NodeNextRequest) ? new NodeNextRequest(req) : req
}

protected normalizeRes(
  res: NodeNextResponse | ServerResponse
): NodeNextResponse {
  return !(res instanceof NodeNextResponse) ? new NodeNextResponse(res) : res
}
```
Sources: [packages/next/src/server/next-server.ts:1339-1349](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/next-server.ts#L1339-L1349)

The request handler pipeline ensures that `prepare()` is fired eagerly upon request receipt while catching unhandled rejections prior to execution. When `getRequestHandler()` or `getRequestHandlerWithMetadata()` is invoked, incoming parameters and metadata are bound to request attributes via `setRequestMeta` before routing through the parent handler.

```typescript
public getRequestHandler(): NodeRequestHandler {
  const handler = this.makeRequestHandler()
  if (this.serverOptions.experimentalTestProxy) {
    const { wrapRequestHandlerNode } =
      // eslint-disable-next-line @next/internal/typechecked-require -- experimental/testmode is not built ins next/dist/esm
      require('next/dist/experimental/testmode/server') as typeof import('../experimental/testmode/server')
    return wrapRequestHandlerNode(handler)
  }
  return handler
}

/**
 * @internal - this method is internal to Next.js and should not be used directly by end-users
 */
public getRequestHandlerWithMetadata(meta: RequestMeta): NodeRequestHandler {
  const handler = this.makeRequestHandler()
  return (req, res, parsedUrl) => {
    setRequestMeta(req, meta)
    return handler(req, res, parsedUrl)
  }
}
```
Sources: [packages/next/src/server/next-server.ts:1351-1371](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/next-server.ts#L1351-L1371)

> [!WARNING]
> Calling `ensurePage` directly on `NextNodeServer` outside of development environments throws an immediate invariant error, as on-demand compilation is restricted to development server instances.

```typescript
  throw new Error(
    'Invariant: ensurePage can only be called on the development server'
  )
}
```
Sources: [packages/next/src/server/next-server.ts:1315-1318](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/next-server.ts#L1315-L1318)

### Caching Bridges and Incremental Cache Configuration

`NextNodeServer` manages caching bridges by integrating custom cache handlers and delegating file system operations to `nodeFs`. Custom cache handlers specified in the user configuration are dynamically imported via `dynamicImportEsmDefault`, interoped, and registered using `setCacheHandler`.

```typescript
  const { cacheMaxMemorySize, cacheHandlers } = this.nextConfig
  if (!cacheHandlers) return

  initializeCacheHandlers(cacheMaxMemorySize)
  await registerCustomCacheHandlers(async () => {
    for (const [kind, handler] of Object.entries(cacheHandlers)) {
      if (!handler) continue

      setCacheHandler(
        kind,
        interopDefault(
          await dynamicImportEsmDefault(
            formatDynamicImportPath(this.distDir, handler)
          )
        )
      )
    }
  })
}
```
Sources: [packages/next/src/server/next-server.ts:431-449](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/next-server.ts#L431-L449)

The [incremental cache](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/server-runtime/incremental-cache) setup method inspects `this.nextConfig` for a `cacheHandler` property, loads any custom handlers asynchronously, and returns the configured cache instance for downstream rendering workers.

```typescript
protected async getIncrementalCache({
  requestHeaders,
}: {
  requestHeaders: IncrementalCache['requestHeaders']
}) {
  const dev = !!this.dev
  let CacheHandler: any
  const { cacheHandler } = this.nextConfig

  if (cacheHandler) {
    CacheHandler = interopDefault(
      await dynamicImportEsmDefault(
        formatDynamicImportPath(this.distDir, cacheHandler)
      )
    )
  }

  await this.loadCustomCacheHandlers()
```
Sources: [packages/next/src/server/next-server.ts:451-468](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/next-server.ts#L451-L468)

| Method / Utility | Target Platform / Module | Purpose |
| :--- | :--- | :--- |
| `getCacheFilesystem()` | `CacheFs` (`nodeFs`) | Supplies the Node.js file system interface for cache persistence |
| `normalizeReq()` | `NodeNextRequest` | Wraps raw Node.js `IncomingMessage` objects into Next.js base request streams |
| `normalizeRes()` | `NodeNextResponse` | Wraps raw Node.js `ServerResponse` objects into Next.js base response streams |
| `handleApiRequest()` | `PagesAPIRouteMatch` | Dispatches legacy Pages API requests via `runApi` execution |

```typescript
protected async handleApiRequest(
  req: NodeNextRequest,
  res: NodeNextResponse,
  query: ParsedUrlQuery,
  match: PagesAPIRouteMatch
): Promise<boolean> {
  return this.runApi(req, res, query, match)
}

protected getCacheFilesystem(): CacheFs {
  return nodeFs
}
```
Sources: [packages/next/src/server/next-server.ts:1326-1337](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/next-server.ts#L1326-L1337)

## HTTP Abstractions and Request Adaptation

### BaseNextRequest and BaseNextResponse Abstractions

Next.js decouples core request handling from platform-specific APIs by defining abstract base classes: `BaseNextRequest` and `BaseNextResponse`. The `BaseNextRequest` abstract class manages request properties including method, URL, body, and headers, while providing a lazy cookie parser utilizing `getCookieParser` configured with request headers.

```typescript
export abstract class BaseNextRequest<Body = any> {
  protected _cookies: NextApiRequestCookies | undefined
  public abstract headers: IncomingHttpHeaders
  public abstract fetchMetrics: FetchMetric[] | undefined

  constructor(
    public method: string,
    public url: string,
    public body: Body
  ) {}

  // Utils implemented using the abstract methods above

  public get cookies() {
    if (this._cookies) return this._cookies
    return (this._cookies = getCookieParser(this.headers)())
  }
}
```
Sources: [packages/next/src/server/base-http/index.ts:28-45](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/base-http/index.ts#L28-L45)

`BaseNextResponse` defines abstract interfaces for status codes, headers, body setting, and response dispatching. It includes helper methods like `redirect`, which automatically sets the `Location` header, status code, and a fallback `Refresh` header if the status code is a permanent redirect.

```typescript
export abstract class BaseNextResponse<Destination = any> {
  abstract statusCode: number | undefined
  abstract statusMessage: string | undefined
  abstract get sent(): boolean

  constructor(public destination: Destination) {}

  /**
   * Sets a value for the header overwriting existing values
   */
  abstract setHeader(name: string, value: string | string[]): this

  /**
   * Removes a header
   */
  abstract removeHeader(name: string): this

  /**
   * Appends value for the given header name
   */
  abstract appendHeader(name: string, value: string): this

  /**
   * Get all values for a header as an array or undefined if no value is present
   */
  abstract getHeaderValues(name: string): string[] | undefined

  abstract hasHeader(name: string): boolean

  /**
   * Get values for a header concatenated using `,` or undefined if no value is present
   */
  abstract getHeader(name: string): string | undefined

  abstract getHeaders(): OutgoingHttpHeaders

  abstract body(value: string): this

  abstract send(): void

  abstract onClose(callback: () => void): void

  // Utils implemented using the abstract methods above

  public redirect(destination: string, statusCode: number) {
    this.setHeader('Location', destination)
    this.statusCode = statusCode

    // Since IE11 doesn't support the 308 header add backwards
    // compatibility using refresh header
    if (statusCode === RedirectStatusCode.PermanentRedirect) {
      this.setHeader('Refresh', `0;url=${destination}`)
    }

    return this
  }
```
Sources: [packages/next/src/server/base-http/index.ts:47-102](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/base-http/index.ts#L47-L102)

### Node.js Platform Stream Implementation

The `NodeNextRequest` and `NodeNextResponse` classes extend the base abstractions to wrap Node.js `IncomingMessage` and `ServerResponse` objects. `NodeNextRequest` exposes a `stream()` method that converts the underlying Node.js readable stream into a Web `ReadableStream`, ensuring the stream can only be read once.

```typescript
export class NodeNextRequest extends BaseNextRequest<Readable> {
  public headers = this._req.headers
  public fetchMetrics: FetchMetric[] | undefined = this._req?.fetchMetrics;

  [NEXT_REQUEST_META]: RequestMeta = this._req[NEXT_REQUEST_META] || {}

  constructor(private _req: Req) {
    super(_req.method!.toUpperCase(), _req.url!, _req)
  }

  get originalRequest() {
    // Need to mimic these changes to the original req object for places where we use it:
    // render.tsx, api/ssg requests
    this._req[NEXT_REQUEST_META] = this[NEXT_REQUEST_META]
    this._req.url = this.url
    this._req.cookies = this.cookies
    return this._req
  }

  set originalRequest(value: Req) {
    this._req = value
  }

  private streaming = false

  /**
   * Returns the request body as a Web Readable Stream. The body here can only
   * be read once as the body will start flowing as soon as the data handler
   * is attached.
   *
   * @internal
   */
  public stream() {
    if (this.streaming) {
      throw new Error(
        'Invariant: NodeNextRequest.stream() can only be called once'
      )
    }
    this.streaming = true

    return new ReadableStream({
      start: (controller) => {
        this._req.on('data', (chunk) => {
          controller.enqueue(new Uint8Array(chunk))
        })
        this._req.on('end', () => {
          controller.close()
        })
        this._req.on('error', (err) => {
          controller.error(err)
        })
      },
    })
  }
}
```
Sources: [packages/next/src/server/base-http/node.ts:19-73](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/base-http/node.ts#L19-L73)

> [!WARNING]
> Calling `NodeNextRequest.stream()` more than once triggers an invariant error stating that the stream can only be called once, preventing multiple consumers from reading a consumed request body.

```typescript
public stream() {
  if (this.streaming) {
    throw new Error(
      'Invariant: NodeNextRequest.stream() can only be called once'
    )
  }
  this.streaming = true
```
Sources: [packages/next/src/server/base-http/node.ts:51-57](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/base-http/node.ts#L51-L57)

`NodeNextResponse` delegates header manipulation, status code setting, and response finalization directly to the underlying Node.js `ServerResponse` instance.

```typescript
export class NodeNextResponse extends BaseNextResponse<Writable> {
  private textBody: string | undefined = undefined

  public [SYMBOL_CLEARED_COOKIES]?: boolean

  get originalResponse() {
    if (SYMBOL_CLEARED_COOKIES in this) {
      this._res[SYMBOL_CLEARED_COOKIES] = this[SYMBOL_CLEARED_COOKIES]
    }

    return this._res
  }

  constructor(
    private _res: ServerResponse & { [SYMBOL_CLEARED_COOKIES]?: boolean }
  ) {
    super(_res)
  }

  get sent() {
    return this._res.finished || this._res.headersSent
  }

  get statusCode() {
    return this._res.statusCode
  }

  set statusCode(value: number) {
    this._res.statusCode = value
  }

  get statusMessage() {
    return this._res.statusMessage
  }

  set statusMessage(value: string) {
    this._res.statusMessage = value
  }

  setHeader(name: string, value: string | string[]): this {
    this._res.setHeader(name, value)
    return this
  }

  removeHeader(name: string): this {
    this._res.removeHeader(name)
    return this
  }

  getHeaderValues(name: string): string[] | undefined {
    const values = this._res.getHeader(name)

    if (values === undefined) return undefined

    return (Array.isArray(values) ? values : [values]).map((value) =>
      value.toString()
    )
  }

  hasHeader(name: string): boolean {
    return this._res.hasHeader(name)
  }

  getHeader(name: string): string | undefined {
    const values = this.getHeaderValues(name)
    return Array.isArray(values) ? values.join(',') : undefined
  }

  getHeaders(): OutgoingHttpHeaders {
    return this._res.getHeaders()
  }

  appendHeader(name: string, value: string): this {
    const currentValues = this.getHeaderValues(name) ?? []

    if (!currentValues.includes(value)) {
      this._res.setHeader(name, [...currentValues, value])
    }

    return this
```
Sources: [packages/next/src/server/base-http/node.ts:75-169](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/base-http/node.ts#L75-L169)

### Bridging to Web NextRequest via NextRequestAdapter

`NextRequestAdapter` transforms `BaseNextRequest` instances into standard `NextRequest` objects depending on the active execution runtime environment. When running outside the [edge runtime](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/server-runtime/edge-runtime), `fromNodeNodeNextRequest` resolves relative URLs against request metadata and configures the request with a half-duplex body and an abort signal tied to the underlying response lifecycle.

```typescript
export class NextRequestAdapter {
  public static fromBaseNextRequest(
    request: BaseNextRequest,
    signal: AbortSignal
  ): NextRequest {
    if (
      // The type check here ensures that `req` is correctly typed, and the
      // environment variable check provides dead code elimination.
      process.env.NEXT_RUNTIME === 'edge' &&
      isWebNextRequest(request)
    ) {
      return NextRequestAdapter.fromWebNextRequest(request)
    } else if (
      // The type check here ensures that `req` is correctly typed, and the
      // environment variable check provides dead code elimination.
      process.env.NEXT_RUNTIME !== 'edge' &&
      isNodeNextRequest(request)
    ) {
      return NextRequestAdapter.fromNodeNextRequest(request, signal)
    } else {
      throw new Error('Invariant: Unsupported NextRequest type')
    }
  }

  public static fromNodeNextRequest(
    request: NodeNextRequest,
    signal: AbortSignal
  ): NextRequest {
    // HEAD and GET requests can not have a body.
    let body: BodyInit | null = null
    if (request.method !== 'GET' && request.method !== 'HEAD' && request.body) {
      // @ts-expect-error - this is handled by undici, when streams/web land use it instead
      body = request.body
    }

    let url: URL
    if (request.url.startsWith('http')) {
      url = new URL(request.url)
    } else {
      // Grab the full URL from the request metadata.
      const base = getRequestMeta(request, 'initURL')
      if (!base || !base.startsWith('http')) {
        // Because the URL construction relies on the fact that the URL provided
        // is absolute, we need to provide a base URL. We can't use the request
        // URL because it's relative, so we use a dummy URL instead.
        url = new URL(request.url, 'http://n')
      } else {
        url = new URL(request.url, base)
      }
    }

    return new NextRequest(url, {
      method: request.method,
      headers: fromNodeOutgoingHttpHeaders(request.headers),
      duplex: 'half',
      signal,
      // geo
      // ip
      // nextConfig

      // body can not be passed if request was aborted
      // or we get a Request body was disturbed error
      ...(signal.aborted
        ? {}
        : {
            body,
          }),
    })
  }
```
Sources: [packages/next/src/server/web/spec-extension/adapters/next-request.ts:56-124](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/web/spec-extension/adapters/next-request.ts#L56-L124)

> [!IMPORTANT]
> If a request signal has already been aborted, `NextRequestAdapter` omits the request body entirely from the constructed `NextRequest` to prevent disturbing or consuming an aborted body stream.

```typescript
  // body can not be passed if request was aborted
  // or we get a Request body was disturbed error
  ...(signal.aborted
    ? {}
    : {
        body,
      }),
})
```
Sources: [packages/next/src/server/web/spec-extension/adapters/next-request.ts:116-123](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/web/spec-extension/adapters/next-request.ts#L116-L123)

| Adapter Method | Target Input Type | Output Type | Purpose |
| :--- | :--- | :--- | :--- |
| `fromBaseNextRequest()` | `BaseNextRequest` | `NextRequest` | Runtime-dispatched entrypoint routing to node or web adapters |
| `fromNodeNextRequest()` | `NodeNextRequest` | `NextRequest` | Converts Node.js incoming messages and metadata into standard `NextRequest` |
| `fromWebNextRequest()` | `WebNextRequest` | `NextRequest` | Adapts web-native request wrappers for standard spec consumption |

```typescript
export class NextRequestAdapter {
  public static fromBaseNextRequest(
    request: BaseNextRequest,
    signal: AbortSignal
  ): NextRequest {
    if (
      // The type check here ensures that `req` is correctly typed, and the
      // environment variable check provides dead code elimination.
      process.env.NEXT_RUNTIME === 'edge' &&
      isWebNextRequest(request)
    ) {
      return NextRequestAdapter.fromWebNextRequest(request)
    } else if (
      // The type check here ensures that `req` is correctly typed, and the
      // environment variable check provides dead code elimination.
      process.env.NEXT_RUNTIME !== 'edge' &&
      isNodeNextRequest(request)
    ) {
      return NextRequestAdapter.fromNodeNextRequest(request, signal)
    } else {
      throw new Error('Invariant: Unsupported NextRequest type')
    }
  }
```
Sources: [packages/next/src/server/web/spec-extension/adapters/next-request.ts:56-78](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/web/spec-extension/adapters/next-request.ts#L56-L78)

## Server Error Handling and Normalization

### Error Types and Safe Stringification

Next.js defines the `NextError` interface extending standard errors to include optional properties like `type`, `page`, `code`, `cancelled`, and `digest`. The `isError` utility validates whether an unknown thrown value is an object containing `name` and `message` properties.

```typescript
export interface NextError extends Error {
  type?: string
  page?: string
  code?: string | number
  cancelled?: boolean
  digest?: string
}
```
Sources: [packages/next/src/lib/is-error.ts:4-10](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/lib/is-error.ts#L4-L10)

```typescript
export default function isError(err: unknown): err is NextError {
  return (
    typeof err === 'object' && err !== null && 'name' in err && 'message' in err
  )
}
```
Sources: [packages/next/src/lib/is-error.ts:38-42](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/lib/is-error.ts#L38-L42)

The `safeStringifyLite` helper serializes objects without requiring external production dependencies by tracking circular references with a `WeakSet` and replacing duplicates with `"[Circular]"`.

```typescript
function safeStringifyLite(obj: any) {
  const seen = new WeakSet()

  return JSON.stringify(obj, (_key, value) => {
    // If value is an object and already seen, replace with "[Circular]"
    if (typeof value === 'object' && value !== null) {
      if (seen.has(value)) {
        return '[Circular]'
      }
      seen.add(value)
    }
    return value
  })
}
```
Sources: [packages/next/src/lib/is-error.ts:19-32](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/lib/is-error.ts#L19-L32)

### Error Normalization and Development Diagnostics

The `getProperError` function normalizes thrown values into standard `Error` instances. In development mode, throwing `undefined` or `null` triggers explicit errors referencing documentation URLs for diagnostics.

```typescript
export function getProperError(err: unknown): Error {
  if (isError(err)) {
    return err
  }

  if (process.env.NODE_ENV === 'development') {
    // provide better error for case where `throw undefined`
    // is called in development
    if (typeof err === 'undefined') {
      return new Error(
        'An undefined error was thrown, ' +
          'see here for more info: https://nextjs.org/docs/messages/threw-undefined'
      )
    }

    if (err === null) {
      return new Error(
        'A null error was thrown, ' +
          'see here for more info: https://nextjs.org/docs/messages/threw-undefined'
      )
    }
  }
```
Sources: [packages/next/src/lib/is-error.ts:44-65](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/lib/is-error.ts#L44-L65)

> [!WARNING]
> Plain objects passed as thrown values are converted to JSON strings using `safeStringifyLite`, while primitive values are coerced into strings before being wrapped in an `Error`.

```typescript
return new Error(isPlainObject(err) ? safeStringifyLite(err) : err + '')
```
Sources: [packages/next/src/lib/is-error.ts:67-67](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/lib/is-error.ts#L67-L67)

| Helper Function | Input Type | Output Type | Purpose |
| :--- | :--- | :--- | :--- |
| `isError()` | `unknown` | `err is NextError` | Type guard checking for object structure with `name` and `message` |
| `getProperError()` | `unknown` | `Error` | Normalizes non-error throwables, handling `undefined`, `null`, and plain objects |
| `safeStringifyLite()` | `any` | `string` | Safe JSON serializer guarding against circular references using a `WeakSet` |

```typescript
function safeStringifyLite(obj: any) {
  const seen = new WeakSet()

  return JSON.stringify(obj, (_key, value) => {
    // If value is an object and already seen, replace with "[Circular]"
    if (typeof value === 'object' && value !== null) {
      if (seen.has(value)) {
        return '[Circular]'
      }
      seen.add(value)
    }
    return value
  })
}

/**
 * Checks whether the given value is a NextError.
 * This can be used to print a more detailed error message with properties like `code` & `digest`.
 */
export default function isError(err: unknown): err is NextError {
  return (
    typeof err === 'object' && err !== null && 'name' in err && 'message' in err
  )
}
```
Sources: [packages/next/src/lib/is-error.ts:19-42](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/lib/is-error.ts#L19-L42)

## Related

- [Request Lifecycle](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/server-runtime/request-lifecycle)
- [Route Handlers](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/server-runtime/route-handlers)
- [Edge Runtime](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/server-runtime/edge-runtime)


## Sitemap

See the full [sitemap](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/llms.txt) for all pages in this wiki.
