Skip to main content
doc0blade47/next.jsdocumentation home
Sign in

Development Server

Last updated Oct 12, 2026100% grounded
Relevant source files

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

Overview

The development server manages the runtime execution, on-demand compilation, routing, and hot module replacement loop for Next.js applications in development. Built around the core Server Architecture, DevServer extends Server (inheriting from NextNodeServer) to initialize development lifecycles, coordinate with compiler services, and serve virtual filesystem items packages/next/src/server/dev/next-dev-server.ts:38-127. It bridges incoming HTTP requests with bundler instances like Webpack and Turbopack via DevBundlerService and router middleware packages/next/src/server/dev/next-dev-server.ts:117-119, packages/next/src/server/lib/dev-bundler-service.ts:20-31. The development server solves the problem of rapid, lazy compilation and continuous feedback during local development by resolving dynamic route segments, watching file modifications, streaming compilation errors packages/next/src/server/dev/hot-reloader-turbopack.ts:1968-2032, and logging request metrics and stack frames packages/next/src/server/dev/log-requests.ts:43-82.

Sources: packages/next/src/server/dev/next-dev-server.ts:38-127, packages/next/src/server/dev/hot-reloader-turbopack.ts:1968-2032, packages/next/src/server/dev/log-requests.ts:43-82, packages/next/src/server/lib/dev-bundler-service.ts:20-31

DevServer architecture and class inheritance

DevServer Public Class Interface and Inheritance

DevServer is exported as the default class from packages/next/src/server/dev/next-dev-server.ts and extends Server, which corresponds to NextNodeServer. The class overrides the nextConfig property type to enforce the complete configuration type NextConfigComplete rather than the runtime-only subset. Its primary state includes a readiness deferred promise created via createPromiseWithResolvers<void>(), sorted route lists, path references for pages and app directories, actual middleware and instrumentation file paths, a bundlerService handle of type DevBundlerService, an LRU cache for static paths, a startup trace span, and cache structures for server component hot module replacement.

export interface Options extends ServerOptions {
  // Override type to make the full config available instead of only NextConfigRuntime
  conf: NextConfigComplete
  /**
   * Tells of Next.js is running from the `next dev` command
   */
  isNextDevCommand?: boolean
 
  /**
   * Interface to the development bundler.
   */
  bundlerService: DevBundlerService
 
  /**
   * Trace span for server startup.
   */
  startServerSpan: Span
}
 
export default class DevServer extends Server {
  // Override type to make the full config available instead of only NextConfigRuntime
  protected readonly nextConfig: NextConfigComplete
 
  /**
   * The promise that resolves when the server is ready. When this is unset
   * the server is ready.
   */
  private ready? = createPromiseWithResolvers<void>()
  protected sortedRoutes?: string[]
  private pagesDir?: string
  private appDir?: string
  private actualMiddlewareFile?: string
  private actualInstrumentationHookFile?: string
  private middleware?: MiddlewareRoutingItem
  private readonly bundlerService: DevBundlerService
  private staticPathsCache: LRUCache<
    UnwrapPromise<ReturnType<DevServer['getStaticPaths']>>
  >
  private startServerSpan: Span
  private readonly serverComponentsHmrCache:
    | ServerComponentsHmrCache
    | undefined

Sources: packages/next/src/server/dev/next-dev-server.ts:108-149

Note

The ready promise field on DevServer acts as an internal gatekeeper: while unset or pending, incoming HTTP requests and execution runs await its resolution before proceeding through super.handleRequest and super.run.

Sources: packages/next/src/server/dev/next-dev-server.ts:135-135, packages/next/src/server/dev/next-dev-server.ts:525-525, packages/next/src/server/dev/next-dev-server.ts:546-546

Server Lifecycle Instantiation and Wrapper Coordination

The factory orchestration layer is governed by NextServer inside packages/next/src/server/next.ts. When next dev is executed, NextServer.createServer inspects the development flag (options.dev). If dev is true, it dynamically imports and instantiates the DevServer implementation exported by packages/next/src/server/dev/next-dev-server.ts.

private async createServer(
  options: ServerOptions | DevServerOptions
): Promise<NextNodeServer> {
  let ServerImplementation: typeof NextNodeServer
  if (options.dev) {
    ServerImplementation = (
      require('./dev/next-dev-server') as typeof import('./dev/next-dev-server')
    ).default as typeof import('./dev/next-dev-server').default
  } else {
    ServerImplementation = await getServerImpl()
  }
  const server = new ServerImplementation(options)
 
  return server
}

Sources: packages/next/src/server/next.ts:326-340

Warning

Attempting to run next start with an output: 'export' or output: 'standalone' configuration triggers immediate validation failures or warnings in NextServer.getServer(), blocking production server instances from inadvertently bootstrapping development infrastructure.

Sources: packages/next/src/server/next.ts:390-401

Bundler integration and entry coordination

DevBundlerService Architecture

DevBundlerService acts as an intermediary layer linking DevServer to the underlying development bundler. It maintains an internal LRU cache for incremental static regeneration (ISR) manifests with a maximum capacity of $8,000$ entries, where each entry is assigned a fixed length weight of $16$. The service binds hot reloader methods such as setCacheStatus, setReactDebugChannel, and sendErrorsToBrowser directly from the bundler instance. When request insights are enabled via configuration, it subscribes to insight updates and dispatches them across WebSockets as REQUEST_INSIGHTS_UPDATE messages.

export class DevBundlerService {
  public appIsrManifestInner: InstanceType<typeof LRUCache<boolean>>
  public setCacheStatus: NextJsHotReloaderInterface['setCacheStatus']
  public setReactDebugChannel: NextJsHotReloaderInterface['setReactDebugChannel']
  public sendErrorsToBrowser: NextJsHotReloaderInterface['sendErrorsToBrowser']
  private unsubscribeRequestInsights?: () => void
 
  constructor(
    private readonly bundler: DevBundler,
    private readonly handler: WorkerRequestHandler,
    requestInsightsEnabled: boolean
  ) {
    this.appIsrManifestInner = new LRUCache(
      8_000,
 
      function length() {
        return 16
      }
    )
 
    const { hotReloader } = bundler
 
    this.setCacheStatus = hotReloader.setCacheStatus.bind(hotReloader)
    this.setReactDebugChannel =
      hotReloader.setReactDebugChannel.bind(hotReloader)
    this.sendErrorsToBrowser = hotReloader.sendErrorsToBrowser.bind(hotReloader)
 
    if (requestInsightsEnabled) {
      this.unsubscribeRequestInsights = subscribeRequestInsights((insight) => {
        hotReloader.send({
          type: HMR_MESSAGE_SENT_TO_BROWSER.REQUEST_INSIGHTS_UPDATE,
          insight,
        })
      })
    }
  }

Sources: packages/next/src/server/lib/dev-bundler-service.ts:20-55

Note

The ISR manifest map is serialized into a plain object when requested via the appIsrManifest getter, but transmission via setIsrStatus is restricted to legacy Pages Router clients or App Router clients with Cache Components disabled.

Sources: packages/next/src/server/lib/dev-bundler-service.ts:128-155

Setup Options and Virtual Manifests

The initialization procedure orchestrated by setupDevBundler accepts a SetupOpts configuration record and yields a set of server fields, a hot reloader interface, a request handler, and error-logging utilities. It registers virtual filesystem items into the file system checker (opts.fsChecker.devVirtualFsItems) to intercept requests targeting runtime development manifests.

Virtual Manifest PathPurpose / Description
/_next/static/development/_devclient-pages-manifest.jsonExposes filtered sort routes excluding app directory files for legacy client consumption.
/_next/static/development/_devmiddleware-manifest.jsonExposes active middleware matchers and routing rules.
/_next/static/development/_turbopack-middleware-manifest.jsonExposes Turbopack-specific middleware matchers and routing patterns.
export type SetupOpts = {
  renderServer: LazyRenderServerInstance
  dir: string
  turbo?: boolean
  appDir?: string
  pagesDir?: string
  telemetry: Telemetry
  isCustomServer?: boolean
  fsChecker: UnwrapPromise<
    ReturnType<typeof import('./filesystem').setupFsCheck>
  >
  nextConfig: NextConfigComplete
  port: number
  onDevServerCleanup: ((listener: () => Promise<void>) => void) | undefined
  resetFetch: () => void
  serverFastRefresh?: boolean
  hasVulnerabilityInsight: Promise<boolean>
}

Sources: packages/next/src/server/lib/router-utils/setup-dev-bundler.ts:128-145, packages/next/src/server/lib/router-utils/setup-dev-bundler.ts:1496-1533

Warning

The virtual manifest request handler checks path inclusion via pathname.includes(...). Requests matching these paths terminate early with a 200 status code and a JSON_CONTENT_TYPE_HEADER header, bypassing standard disk reads.

Sources: packages/next/src/server/lib/router-utils/setup-dev-bundler.ts:1505-1533

Lifecycle Hooks and Error Formatting

DevBundlerService wraps bundler operations with tracing spans and error deobfuscation. Its ensurePage method wraps the hot reloader's page compilation call inside a DevBundlerServiceSpan.ensurePage trace span labeled with the name 'compile route'. Similarly, logErrorWithOriginalStack deobfuscates incoming error text strings using deobfuscateText before evaluating whether the error instance represents a ModuleBuildError or a TurbopackInternalError.

public ensurePage: typeof this.bundler.hotReloader.ensurePage = async (
  definition
) => {
  // TODO: remove after ensure is pulled out of server
  return await getTracer().trace(
    DevBundlerServiceSpan.ensurePage,
    { spanName: 'compile route' },
    () => this.bundler.hotReloader.ensurePage(definition)
  )
}

Sources: packages/next/src/server/lib/dev-bundler-service.ts:62-71, packages/next/src/server/lib/router-utils/setup-dev-bundler.ts:1535-1557

On-demand compilation and page entry handling

Dynamic Entry Resolution and Route Key Generation

Next.js manages development compilation lazily by resolving entry points on demand when requests hit the server. The on-demand entry handler constructs unique keys for individual page bundles using getEntryKey, which combines the compiler type, page bundle type, and normalized page path to prevent naming collisions between the app/ and pages/ directories.

export function getEntryKey(
  compilerType: CompilerNameValues,
  pageBundleType: PAGE_TYPES,
  page: string
) {
  // TODO: handle the /children slot better
  // this is a quick hack to handle when children is provided as children/page instead of /page
  const pageKey = page.replace(/(@[^/]+)\/children/g, '$1')
  return `${compilerType}@${pageBundleType}@${pageKey}`
}

Sources: packages/next/src/server/dev/on-demand-entry-handler.ts:117-126

Entry resolution relies on matching bundle file paths against specific regex patterns to extract the target route. getRouteFromEntrypoint delegates to matchBundle across server route patterns, browser route patterns, and app route patterns.

Function / HelperTarget Pattern RegexPurpose
SERVER_ROUTE_NAME_REGEX^pages[/\\](.*)$Matches server-side pages directory entry files.
BROWSER_ROUTE_NAME_REGEX^static[/\\]pages[/\\](.*)$Matches client-side pages directory entry files.
APP_ROUTE_NAME_REGEX^app[/\\](.*)$Matches app directory entry files via getAppRouteFromEntrypoint.
// matches pages/:page*.js
const SERVER_ROUTE_NAME_REGEX = /^pages[/\\](.*)$/
 
// matches static/pages/:page*.js
const BROWSER_ROUTE_NAME_REGEX = /^static[/\\]pages[/\\](.*)$/
 
export default function getRouteFromEntrypoint(
  entryFile: string,
  app?: boolean
): string | null {
  let pagePath = matchBundle(SERVER_ROUTE_NAME_REGEX, entryFile)
 
  if (pagePath) {
    return pagePath
  }
 
  if (app) {
    pagePath = getAppRouteFromEntrypoint(entryFile)
    if (pagePath) return pagePath
  }
 
  // Potentially the passed item is a browser bundle so we try to match that also
  return matchBundle(BROWSER_ROUTE_NAME_REGEX, entryFile)

Sources: packages/next/src/server/get-route-from-entrypoint.ts:4-26, packages/next/src/server/get-app-route-from-entrypoint.ts:4-16, packages/next/src/server/match-bundle.ts:3-14

Note

Caches and batchers ensure that concurrent requests targeting the same uncompiled page coalesce into a single compilation pass rather than triggering redundant worker tasks.

Sources: packages/next/src/server/dev/on-demand-entry-handler.ts:1040-1081

Lazy Compilation and ensurePage

The developer server triggers lazy compilation through the ensurePage method exposed by the entry handler. Incoming compilation options (EnsurePageOptions) are processed through a batcher utilizing scheduleOnNextTick to schedule execution and a serialization cache key function to prevent concurrent duplicate compilations for identical pages.

type EnsurePageOptions = {
  page: string
  appPaths?: ReadonlyArray<string> | null
  definition?: RouteDefinition
  isApp?: boolean
  url?: string
}
 
// Make sure that we won't have multiple invalidations ongoing concurrently.
const batcher = Batcher.create<EnsurePageOptions, void, string>({
  // The cache key here is composed of the elements that affect the
  // compilation, namely, the page, whether it's client only, and whether
  // it's an app page. This ensures that we don't have multiple compilations
  // for the same page happening concurrently.
  //
  // We don't include the whole match because it contains match specific
  // parameters (like route params) that would just bust this cache. Any
  // details that would possibly bust the cache should be listed here.
  cacheKeyFn: (options) => JSON.stringify(options),
  // Schedule the invocation of the ensurePageImpl function on the next tick.
  schedulerFn: scheduleOnNextTick,
})
 
return {
  async ensurePage({
    page,
    appPaths = null,
    definition,
    isApp,
    url,
  }: EnsurePageOptions) {
    // If the route is actually an app page route, then we should have access
    // to the app route definition, and therefore, the appPaths from it.
    if (!appPaths && definition && isAppPageRouteDefinition(definition)) {
      appPaths = definition.appPaths
    }
 
    // Wrap the invocation of the ensurePageImpl function in the pending
    // wrapper, which will ensure that we don't have multiple compilations
    // for the same page happening concurrently.
    return batcher.batch({ page, appPaths, definition, isApp }, async () => {
      await ensurePageImpl({
        page,
        appPaths,
        definition,
        isApp,
        url,
      })
    })
  },

Sources: packages/next/src/server/dev/on-demand-entry-handler.ts:1032-1081

Dynamic route matching and path resolution

Dev Route Normalization and App Path Transformation

Next.js normalizes file-system paths within the app/ and pages/ directories into clean request pathnames. The normalizeAppPath function processes app routes by iterating through path segments, stripping empty items, parallel route slots (segments starting with @), route groups, and leaf identifiers like page or route at the end of the array, while ensureConfiguredLeading or ensureLeadingSlash guarantees a leading slash on the result.

export function normalizeAppPath(route: string) {
  return ensureLeadingSlash(
    route.split('/').reduce((pathname, segment, index, segments) => {
      if (!segment) return pathname
      if (isGroupSegment(segment)) return pathname
      if (segment[0] === '@') return pathname
      if ((segment === 'page' || segment === 'route') && index === segments.length - 1) {
        return pathname
      }
      return `${pathname}/${segment}`
    }, '')
  )
}

Sources: packages/next/src/shared/lib/page-path/ensure-leading-slash.ts:5-7, packages/next/src/shared/lib/router/utils/app-paths.ts:23-52

Asset paths generated by the compiler are mapped back to logical routes via getRouteFromAssetPath, which strips extensions and cleans up /index prefix rules for non-dynamic assets. When sorting app paths to select a canonical entry point via compareAppPaths, paths containing parallel slots (/@) are prioritized over root page paths to prevent manifest resolution mismatches.

export default function getRouteFromAssetPath(
  assetPath: string,
  ext: string = ''
): string {
  assetPath = assetPath.replace(/\\/g, '/')
  assetPath =
    ext && assetPath.endsWith(ext) ? assetPath.slice(0, -ext.length) : assetPath
  if (assetPath.startsWith('/index/') && !isDynamicRoute(assetPath)) {
    assetPath = assetPath.slice(6)
  } else if (assetPath === '/index') {
    assetPath = '/'
  }
  return assetPath

Sources: packages/next/src/shared/lib/router/utils/get-route-from-asset-path.ts:6-18, packages/next/src/shared/lib/router/utils/app-paths.ts:63-69

Dynamic Segment Detection

Route paths are evaluated for parameterization through isDynamicRoute, which inspects path strings using regular expression matchers. Depending on the strict flag parameter, it toggles between strict segment matching and loose wildcard parsing.

Regular Expression NameExpression ValueMatching Behavior
TEST_STRICT_ROUTE`//[[^/]+](?=/$)/`
TEST_ROUTE`//[^/][[^/]+][^/](?=/$)/`
// Identify /.*[param].*/ in route string
const TEST_ROUTE = /\/[^/]*\[[^/]+\][^/]*(?=\/|$)/
 
// Identify /[param]/ in route string
const TEST_STRICT_ROUTE = /\/\[[^/]+\](?=\/|$)/
 
/**
 * Check if a route is dynamic.
 *
 * @param route - The route to check.
 * @param strict - Whether to use strict mode which prohibits segments with prefixes/suffixes (default: true).
 * @returns Whether the route is dynamic.
 */
export function isDynamicRoute(route: string, strict: boolean = true): boolean {
  if (isInterceptionRouteAppPath(route)) {
    route = extractInterceptionRouteInformation(route).interceptedRoute
  }
 
  if (strict) {
    return TEST_STRICT_ROUTE.test(route)
  }
 
  return TEST_ROUTE.test(route)

Sources: packages/next/src/shared/lib/router/utils/is-dynamic.ts:6-28

Note

When checking interception route app paths, isDynamicRoute automatically unwraps the intercepted sub-route portion before running the regex test.

Sources: packages/next/src/shared/lib/router/utils/is-dynamic.ts:20-22

Interception Route Handling

Interception routes allow loading a component from another part of the app within the current layout context. The route parser recognizes four distinct structural markers defined in INTERCEPTION_ROUTE_MARKERS.

Interception MarkerNavigation ScopeTarget Transformation Rule
(.)Sibling directoryAppends the intercepted path directly to the normalized intercepting route.
(..)One level upRemoves the last segment of the intercepting route before appending.
(...)Root directory (/)Prepends root-level path resolution to the intercepted segment.
(..)(..)Two levels upRemoves the last two segments of the intercepting route before appending.
export const INTERCEPTION_ROUTE_MARKERS = [
  '(..)(..)',
  '(.)',
  '(..)',
  '(...)',
] as const

Sources: packages/next/src/shared/lib/router/utils/interception-routes.ts:4-9, packages/next/src/shared/lib/router/utils/interception-routes.ts:168-211

Sources: packages/next/src/shared/lib/router/utils/interception-routes.ts:119-214

Warning

Using the (..) marker at the root level or (..)(..)() at the root or one level up throws an immediate invariant error because insufficient parent segments exist for traversal.

Sources: packages/next/src/shared/lib/router/utils/interception-routes.ts:179-183, packages/next/src/shared/lib/router/utils/interception-routes.ts:198-202

Development request handling and logging

Request Execution and Routing Wrappers

Incoming HTTP requests in the development server flow through handleRequest and run methods on DevServer. The handleRequest implementation initializes a trace span named handle-request, awaits server readiness via this.ready?.promise, attaches PagesErrorDebug metadata to the request, and delegates execution to super.handleRequest. Immediately following execution, a child span records process memory usage metrics including memory.rss, memory.heapUsed, and memory.heapTotal before stopping.

  const span = trace('handle-request', undefined, { url: req.url })
  const result = await span.traceAsyncFn(async () => {
    await this.ready?.promise
    addRequestMeta(req, 'PagesErrorDebug', this.renderOpts.ErrorDebug)
    return await super.handleRequest(req, res, parsedUrl)
  })
  const memoryUsage = process.memoryUsage()
  span
    .traceChild('memory-usage', {
      url: req.url,
      'memory.rss': String(memoryUsage.rss),
      'memory.heapUsed': String(memoryUsage.heapUsed),
      'memory.heapTotal': String(memoryUsage.heapTotal),
    })
    .stop()
  return result
}

Sources: packages/next/src/server/dev/next-dev-server.ts:523-539

The run method handles basePath prefix stripping from parsedUrl.pathname before invoking parent execution routines, restoring the original pathname afterward so custom routes match correctly. It guards against public directory conflicts by verifying whether /_next exists inside this.publicDir. Any unhandled exceptions thrown during request processing are caught, formatted via formatServerError, passed to this.logErrorWithOriginalStack, and handled by rendering an error response with a 500 status code.

async run(
  req: NodeNextRequest,
  res: NodeNextResponse,
  parsedUrl: UrlWithParsedQuery
): Promise<void> {
  await this.ready?.promise
 
  const { basePath } = this.nextConfig
  let originalPathname: string | null = null
 
  // TODO: see if we can remove this in the future
  if (basePath && pathHasPrefix(parsedUrl.pathname || '/', basePath)) {
    // strip basePath before handling dev bundles
    // If replace ends up replacing the full url it'll be `undefined`, meaning we have to default it to `/`
    originalPathname = parsedUrl.pathname
    parsedUrl.pathname = removePathPrefix(parsedUrl.pathname || '/', basePath)
  }
 
  const { pathname } = parsedUrl
 
  if (pathname!.startsWith('/_next')) {
    if (fs.existsSync(pathJoin(this.publicDir, '_next'))) {
      throw new Error(PUBLIC_DIR_MIDDLEWARE_CONFLICT)
    }
  }
 
  if (originalPathname) {
    // restore the path before continuing so that custom-routes can accurately determine
    // if they should match against the basePath or not
    parsedUrl.pathname = originalPathname
  }
  try {
    return await super.run(req, res, parsedUrl)
  } catch (error) {
    const err = getProperError(error)
    formatServerError(err)
    this.logErrorWithOriginalStack(err)
    if (!res.sent) {
      res.statusCode = 500
      try {
        return await this.renderError(err, req, res, pathname!, {
          __NEXT_PAGE: (isError(err) && err.page) || pathname || '',
        })
      } catch (internalErr) {
        console.error(internalErr)
        res.body('Internal Server Error').send()
      }
    }
  }
}

Sources: packages/next/src/server/dev/next-dev-server.ts:541-590

Warning

If /_next exists as a physical directory inside the public directory, run throws an immediate PUBLIC_DIR_MIDDLEWARE_CONFLICT error to prevent asset routing collisions.

Sources: packages/next/src/server/dev/next-dev-server.ts:561-565

Dev Console Request-Response Logging

Development request logging is controlled by logRequests and ignoreLoggingIncomingRequests, which filter incoming requests against configured regular expression patterns in loggingConfig.incomingRequests.ignore.

export function ignoreLoggingIncomingRequests(
  request: NodeNextRequest,
  loggingConfig: LoggingConfig | undefined
): boolean {
  // If it's boolean use the boolean value
  if (typeof loggingConfig?.incomingRequests === 'boolean') {
    return !loggingConfig.incomingRequests
  }
 
  // Any of the value on the chain is falsy, will not ignore the request.
  const ignore = loggingConfig?.incomingRequests?.ignore
 
  // If ignore is not set, don't ignore anything
  if (!ignore) {
    return false
  }
 
  // If array of RegExp, ignore if any pattern matches
  return ignore.some((pattern) => pattern.test(request.url))
}

Sources: packages/next/src/server/dev/log-requests.ts:22-41

Status codes are mapped to color formatters based on numeric thresholds to output status badges in the console.

Status Code RangeColor Formatter
< 200white
200 to 299green
300 to 399blue
400 to 499yellow
500+red
const statusCodeColor =
  statusCode < 200
    ? white
    : statusCode < 300
      ? green
      : statusCode < 400
        ? blue
        : statusCode < 500
          ? yellow
          : red

Sources: packages/next/src/server/dev/log-requests.ts:97-106

Timing breakdowns compute durations for middleware (proxy.ts), framework internals (next.js), parameter generation (generate-params), and user application code (application-code). Server action execution logs are written below the main request log when devServerActionLog metadata is present on the request. Fetch metrics attached to request.fetchMetrics are formatted according to cache status values.

Cache Status ValueFormatted Output String
hmr(HMR cache) (green)
hit(cache hit) (green)
miss(cache miss) (yellow)
skip(cache skip) (yellow)
function formatCacheStatus(cacheStatus: FetchMetric['cacheStatus']): string {
  switch (cacheStatus) {
    case 'hmr':
      return green('(HMR cache)')
    case 'hit':
      return green('(cache hit)')
    case 'miss':
    case 'skip':
      return yellow(`(cache ${cacheStatus})`)
    default:
      return cacheStatus satisfies never
  }

Sources: packages/next/src/server/dev/log-requests.ts:225-236

Compilation error collection and propagation

Compilation Error Retrieval and Hot Reloading

The DevBundlerService exposes helpers to retrieve compilation errors from the underlying hot reloader and propagate them to the browser over WebSockets. The getCompilationError method queries the hot reloader for errors associated with a specific page and returns the first reported error entry if present.

public async getCompilationError(page: string) {
  const errors = await this.bundler.hotReloader.getCompilationErrors(page)
  if (!errors) return
 
  // Return the very first error we found.
  return errors[0]
}

Sources: packages/next/src/server/lib/dev-bundler-service.ts:92-98

WebSocket connections established via onHMR listen for incoming client messages and error events. When a new HMR server error is detected and not yet buffered, sendErrorsToBrowser formats the error using stringifyError and broadcasts a SERVER_ERROR message containing the JSON representation to the client.

try {
  const error = getHmrServerError()
 
  // New error occurred: buffered error is flushed and new error occurred
  if (!bufferedHmrServerError && error) {
    hotReloader.send({
      type: HMR_MESSAGE_SENT_TO_BROWSER.SERVER_ERROR,
      errorJSON: stringifyError(error),
    })
    bufferedHmrServerError = null
  }
 

Sources: packages/next/src/server/dev/on-demand-entry-handler.ts:1101-1112

Warning

bufferedHmrServerError is reset to null whenever the WebSocket connection closes, ensuring that stale compilation or HMR errors do not persist across reconnections.

Sources: packages/next/src/server/dev/on-demand-entry-handler.ts:1096-1099

Fallback Error Component Generation

When a fatal compilation failure occurs or an invalid route is requested, the development server constructs fallback error components. The getFallbackErrorComponents method triggers buildFallbackError on the hot reloader and subsequently calls ensurePage for /_error to guarantee that the default error component and its fallback bundle are successfully compiled.

public async getFallbackErrorComponents(url?: string) {
  await this.bundler.hotReloader.buildFallbackError()
  // Build the error page to ensure the fallback is built too.
  // TODO: See if this can be moved into hotReloader or removed.
  await this.bundler.hotReloader.ensurePage({
    page: '/_error',
    clientOnly: false,
    definition: undefined,
    url,
  })
}

Sources: packages/next/src/server/lib/dev-bundler-service.ts:80-90

Server-side error logging uses logErrorWithOriginalStack to map transpiled stack traces back to original source code locations before printing output to the developer console or forwarding them through the dev bundler service.

protected logErrorWithOriginalStack(
  err?: unknown,
  type?: 'unhandledRejection' | 'uncaughtException' | 'warning' | 'app-dir'
): void {
  this.bundlerService.logErrorWithOriginalStack(err, type)
}

Sources: packages/next/src/server/dev/next-dev-server.ts:592-597

Was this page helpful?