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

Instant Validation

Last updated Oct 12, 2026100% grounded
Relevant source files

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

Overview

Instant Validation provides development and build-time verification for React Server Component routes opting into the instant segment configuration, ensuring that prefetched shells and dynamic render paths fulfill sample requirements and boundary constraints. By intercepting route parameters, cookies, and headers, and executing validation passes through dedicated workers, it detects missing samples, unrendered segments, and illegal dynamic access before production deployment.

Sources: packages/next/src/server/app-render/instant-validation/instant-validation.tsx:1-53, packages/next/src/server/app-render/instant-validation/instant-config.tsx:1-48

Segment Configuration and Validation Triggers

Config Resolution and Traversal

[[Instant Validation]] resolves instant validation eligibility by walking loader trees and inspecting segment-level instant and prefetch options. The validation eligibility depends on whether a segment is a page or default segment, whether an explicit instant export or prefetch: 'partial' is configured, and whether unstable_disableValidation, unstable_disableDevValidation, or unstable_disableBuildValidation are set.

async function visit(tree: LoaderTree): Promise<void> {
  if (disabled) return
 
  const { mod: layoutOrPageMod } = await getLayoutOrPageModule(tree)
  const instantConfig = layoutOrPageMod
    ? (layoutOrPageMod as AppSegmentConfig).instant
    : undefined
 
  if (instantConfig === false) {
    // Explicit opt-out. Doesn't itself trigger validation.
  } else if (instantConfig === true) {
    // Explicit opt-in using the default level.
    if (level <= baseValidationLevel) {
      needsValidation = true
    }
  } else if (typeof instantConfig === 'object' && instantConfig !== null) {
    if (
      instantConfig.unstable_disableValidation === true ||
      (level === VALIDATION_LEVEL.WARNING &&
        instantConfig.unstable_disableDevValidation === true) ||
      (level === VALIDATION_LEVEL.ERROR &&
        instantConfig.unstable_disableBuildValidation === true)
    ) {
      disabled = true
      return
    }
 
    if (instantConfig.level !== undefined) {
      const configuredLevel =
        instantConfig.level === 'experimental-error'
          ? VALIDATION_LEVEL.ERROR
          : VALIDATION_LEVEL.WARNING
      if (level <= configuredLevel) {
        needsValidation = true
      }
    } else if (level <= baseValidationLevel) {
      needsValidation = true
    }
  } else if (applyDefaultValidation && isImplicitValidationSegment(tree[0])) {
    // No explicit config. Implicit validation applies to page/default
    // segments when the default level is active for this mode.
    needsValidation = true
  }
 
  const { parallelRoutes } = parseLoaderTree(tree)
  for (const parallelRouteKey in parallelRoutes) {
    await visit(parallelRoutes[parallelRouteKey])
    if (disabled) return
  }
}

Sources: packages/next/src/server/app-render/instant-validation/instant-config.tsx:169-218

Validation Triggers and Modes

The system evaluates validation checks under two distinct execution targets: development mode via anySegmentNeedsInstantValidationInDev and build mode via anySegmentNeedsInstantValidationInBuild. These map directly to VALIDATION_LEVEL.WARNING and VALIDATION_LEVEL.ERROR respectively, governed by the active workStore.validationLevel.

Validation Level SettingValidation Level EnumManual FlagDescription
manual-warningVALIDATION_LEVEL.WARNINGtrueManual warning mode (disables implicit validation)
warningVALIDATION_LEVEL.WARNINGfalseDefault warning mode for development
experimental-manual-errorVALIDATION_LEVEL.ERRORtrueManual error mode (disables implicit validation)
experimental-errorVALIDATION_LEVEL.ERRORfalseDefault error mode for production builds
enum VALIDATION_LEVEL {
  WARNING = 0,
  ERROR = 1,
}
 
/**
 * Walks the loader tree and checks if any segment has an `instant` config
 * that needs validating for the given mode.
 *
 * - Explicit `instant` exports are checked against mode.
 * - Page and default segments without an explicit config get implicit
 *   validation when the default validation level applies to this mode.
 * - `unstable_disableValidation` on any segment kills validation for
 *   the whole tree.
 */
async function anySegmentNeedsInstantValidation(
  rootTree: LoaderTree,
  level: VALIDATION_LEVEL
): Promise<boolean> {
  const workStore = workAsyncStorage.getStore()
  if (!workStore) {
    throw new InvariantError(
      'anySegmentNeedsInstantValidation must run inside a WorkStore'
    )
  }
  const { validationLevel } = workStore
 
  let baseValidationLevel: number = VALIDATION_LEVEL.WARNING
  let manualValidation: boolean = false
  switch (validationLevel) {
    case 'manual-warning':
      manualValidation = true
    // intentional fallthrough
    case 'warning':
      baseValidationLevel = VALIDATION_LEVEL.WARNING
      break
    case 'experimental-manual-error':
      manualValidation = true
    // intentional fallthrough
    case 'experimental-error':
      baseValidationLevel = VALIDATION_LEVEL.ERROR
      break
    default:
      validationLevel satisfies never
  }

Sources: packages/next/src/server/app-render/instant-validation/instant-config.tsx:112-156

Note

Framework-synthesized error and not-found routes (UNDERSCORE_GLOBAL_ERROR_ROUTE and UNDERSCORE_NOT_FOUND_ROUTE) are excluded from implicit validation under non-manual default levels, ensuring that fallback error UI does not trigger unexpected validation failures unless explicitly opted in by the user.

Sources: packages/next/src/server/app-render/instant-validation/instant-config.tsx:33-47, packages/next/src/server/app-render/instant-validation/instant-config.tsx:158-165

Segment Eligibility Rules

Implicit validation applies specifically to page and default segment keys (PAGE_SEGMENT_KEY or DEFAULT_SEGMENT_KEY) when no explicit instant config is present. Layout segments do not validate on their own; instead, segment sample resolution iterates through children slots to collect unstable_samples without merging outer and inner arrays.

export function isImplicitValidationSegment(segment: Segment): boolean {
  const key = typeof segment === 'string' ? segment : segment[0]
  return key === PAGE_SEGMENT_KEY || key === DEFAULT_SEGMENT_KEY
}

Sources: packages/next/src/server/app-render/instant-validation/instant-config.tsx:28-31, packages/next/src/server/app-render/instant-validation/instant-config.tsx:237-272

Warning

Setting unstable_disableValidation: true on any segment within a loader tree immediately short-circuits the entire tree traversal (disabled = true), disabling validation globally for that render request regardless of other configurations.

Sources: packages/next/src/server/app-render/instant-validation/instant-config.tsx:185-194

Boundary Placement and Marker Hierarchy

Context and Boundary Insertion

During the traversal of route segment trees on the server, validation boundaries and slot markers are injected to isolate portions of the React component tree and attribute errors accurately. The injection mechanism relies on InstantValidationBoundaryContext, which accepts a BoundaryPlacement value of either null or a string boundaryId. When PlaceValidationBoundaryBelowThisLevel is invoked, it wraps its children in this context, signaling outer layout routers to render a validation boundary at that specific depth.

type BoundaryPlacement =
  | null // do not place here
  | string // boundaryId -- place here
 
export const InstantValidationBoundaryContext =
  createContext<BoundaryPlacement>(null)
 
export function PlaceValidationBoundaryBelowThisLevel({
  id,
  children,
}: {
  id: string
  children: ReactNode
}) {
  return (
    // OuterLayoutRouter will see this and render a `RenderValidationBoundaryAtThisLevel`.
    <InstantValidationBoundaryContext value={id}>
      {children}
    </InstantValidationBoundaryContext>
  )
}

Sources: packages/next/src/server/app-render/instant-validation/boundary-impl.tsx:62-82

When RenderValidationBoundaryAtThisLevel consumes the boundary ID from the context, it instantiates an InstantValidationBoundary component and immediately resets the inner context value to null. This prevents child segments from redundantly rendering nested validation boundaries.

export function RenderValidationBoundaryAtThisLevel({
  id,
  children,
}: {
  id: string
  children: ReactNode
}) {
  // We got a boundaryId from the context. Clear the context so that the children don't render another boundary.
  return (
    <InstantValidationBoundary id={id}>
      <InstantValidationBoundaryContext value={null}>
        {children}
      </InstantValidationBoundaryContext>
    </InstantValidationBoundary>
  )
}

Sources: packages/next/src/server/app-render/instant-validation/boundary-impl.tsx:84-99

Caution

Instant validation boundary components are strictly prohibited from appearing in browser bundles. If loaded in a browser environment (typeof window !== 'undefined'), the implementation throws an immediate InvariantError.

Sources: packages/next/src/server/app-render/instant-validation/boundary-impl.tsx:13-17

Tracking and Slot Attribution

The inner boundary component is backed by NameSpace keyed via INSTANT_VALIDATION_BOUNDARY_NAME. When rendered, it retrieves the current validation tracking state from workUnitAsyncStorage and registers the rendered boundary ID into the active tracking set.

const NameSpace = {
  [INSTANT_VALIDATION_BOUNDARY_NAME]: function ({
    id,
    children,
  }: {
    id: string
    children: ReactNode
  }) {
    // Track which boundaries we actually managed to render.
    const state = getValidationBoundaryTracking()
    if (state === null) {
      throw new InvariantError('Missing boundary tracking state')
    }
    state.renderedIds.add(id)
 
    return children
  },
}

Sources: packages/next/src/server/app-render/instant-validation/boundary-impl.tsx:43-60

Constant NameValue / FormatPurpose
INSTANT_VALIDATION_BOUNDARY_NAME__next_instant_validation_boundary__Base name for the validation boundary component namespace
INSTANT_SLOT_MARKER_PREFIX__next_instant_slot_Prefix string for dynamic slot marker components
INSTANT_SLOT_MARKER_SUFFIX__Suffix string appended to dynamic slot marker components
export const INSTANT_VALIDATION_BOUNDARY_NAME =
  '__next_instant_validation_boundary__'
 
export const INSTANT_SLOT_MARKER_PREFIX = '__next_instant_slot_'
export const INSTANT_SLOT_MARKER_SUFFIX = '__'

Sources: packages/next/src/server/app-render/instant-validation/boundary-constants.ts:1-5

To attribute validation errors correctly when a boundary spans multiple parallel slots, SlotMarker dynamically caches and instantiates inner wrapper components using a slotMarkerCache map. This ensures that the slot index appears explicitly within the server-rendered component stack under names conforming to the slot marker prefix and suffix conventions.

const slotMarkerCache = new Map<
  string,
  (props: { children: ReactNode }) => ReactNode
>()
 
export function SlotMarker({
  name,
  children,
}: {
  name: string
  children: ReactNode
}) {
  let Marker = slotMarkerCache.get(name)
  if (!Marker) {
    const ns = {
      [name]: function ({ children: c }: { children: ReactNode }) {
        return c
      },
    }
    Marker = ns[name]
    slotMarkerCache.set(name, Marker)
  }
  return <Marker>{children}</Marker>
}

Sources: packages/next/src/server/app-render/instant-validation/boundary-impl.tsx:114-137

Dynamic Hole and Boundary Tracking

Boundary Tracking State and Verification

State management for boundary validation relies on the ValidationBoundaryTracking type, which maintains a map of required boundaries and a set of successfully rendered boundary identifiers. The requiredIds map associates each boundary identifier (corresponding to a segment path) with an array of file paths for modules located inside that boundary's subtree. When a boundary spans multiple parallel slots, each slot contributes its own first-found module path so that all unrendered segments can be reported together.

export type ValidationBoundaryTracking = {
  /**
   * Map from boundary id (the SegmentPath where a validation boundary is
   * placed) to the file paths of modules inside that boundary's subtree.
   * When the boundary spans multiple parallel slots, each slot contributes
   * its own first-found module path so all unrendered segments can be
   * reported together.
   */
  requiredIds: Map<string, string[]>
  renderedIds: Set<string>
}

Sources: packages/next/src/server/app-render/instant-validation/boundary-tracking.tsx:1-11

New tracking instances are initialized via createValidationBoundaryTracking, which optionally accepts a previous tracking state to copy required boundaries into a new Map while starting with a fresh Set for rendered identifiers. Verification that all required boundaries have been rendered is performed by allRequiredBoundariesRendered, which iterates over all keys in requiredIds and checks that every key exists within renderedIds.

export function createValidationBoundaryTracking(
  /** Pass if the render is expected to render the same IDs as a previous one. */
  matchPrevious?: ValidationBoundaryTracking
): ValidationBoundaryTracking {
  return {
    requiredIds: matchPrevious ? new Map(matchPrevious.requiredIds) : new Map(),
    renderedIds: new Set(),
  }
}
 
export function allRequiredBoundariesRendered(
  state: ValidationBoundaryTracking
): boolean {
  for (const id of state.requiredIds.keys()) {
    if (!state.renderedIds.has(id)) {
      return false
    }
  }
  return true
}

Sources: packages/next/src/server/app-render/instant-validation/boundary-tracking.tsx:13-32

Work Store Integration and Boundary Resolution

During server rendering, active boundary tracking state is retrieved through workUnitAsyncStorage. The helper function getValidationBoundaryTracking accesses the current asynchronous work store and inspects its type. If the store type is 'validation-client', it returns the associated boundaryState; for all other store types, it safely yields null.

function getValidationBoundaryTracking(): ValidationBoundaryTracking | null {
  const store = workUnitAsyncStorage.getStore()
  if (!store) return null
  switch (store.type) {
    case 'validation-client':
      return store.boundaryState
    case 'prerender':
    case 'prerender-client':
    case 'prerender-legacy':
    case 'prerender-runtime':
    case 'request':
    case 'cache':
    case 'private-cache':
    case 'unstable-cache':
    case 'build-time-generator':
      break
    default:
      store satisfies never
  }
  return null
}

Sources: packages/next/src/server/app-render/instant-validation/boundary-impl.tsx:19-39

Warning

If getValidationBoundaryTracking encounters a store type that is unhandled by the switch statement, TypeScript's store satisfies never check enforces exhaustive type coverage at compile time, preventing silent validation failures during async store transitions.

Sources: packages/next/src/server/app-render/instant-validation/boundary-impl.tsx:32-37

When an InstantValidationBoundary component mounts during tree construction, it invokes getValidationBoundaryTracking to obtain the active tracking store. If the store yields null, an InvariantError is immediately thrown indicating missing boundary tracking state. Otherwise, the boundary's id is added to state.renderedIds, recording successful render traversal for that segment path.

    // Track which boundaries we actually managed to render.
    const state = getValidationBoundaryTracking()
    if (state === null) {
      throw new InvariantError('Missing boundary tracking state')
    }
    state.renderedIds.add(id)
 
    return children
  },
}

Sources: packages/next/src/server/app-render/instant-validation/boundary-impl.tsx:51-60

Sample Proxies and Parameter Instrumentation

Exhaustive sample validation intercepts runtime data access by wrapping underlying request primitives in JavaScript proxies. The createCookiesFromSample function builds a ReadonlyRequestCookies wrapper from defined cookie sample configurations. It validates that any accessed cookie name exists within the declared sample set. If an undeclared cookie is queried via methods like has or get, the proxy invokes trackMissingSampleErrorAndThrow with an InstantValidationError.

export function createCookiesFromSample(
  sampleCookies: InstantSample['cookies'],
  route: string
): ReadonlyRequestCookies {
  const declaredNames = new Set<string>()
 
  const cookies = new RequestCookies(new Headers())
  if (sampleCookies) {
    for (const cookie of sampleCookies) {
      declaredNames.add(cookie.name)
      if (cookie.value !== null) {
        cookies.set(cookie.name, cookie.value)
      }
    }
  }
 
  const sealed = RequestCookiesAdapter.seal(cookies)
 
  return new Proxy(sealed, {
    get(target, prop, receiver) {
      if (prop === 'has') {
        const originalMethod = Reflect.get(target, prop, receiver)
        const wrappedMethod: typeof originalMethod = function (name) {
          if (!declaredNames.has(name)) {
            trackMissingSampleErrorAndThrow(
              createMissingCookieSampleError(route, name)
            )
          }
          return originalMethod.call(target, name)
        }
        return wrappedMethod
      }
      if (prop === 'get') {
        const originalMethod = Reflect.get(target, prop, receiver)
        const wrappedMethod: typeof originalMethod = function (nameOrCookie) {
          let name: string
          if (typeof nameOrCookie === 'string') {
            name = nameOrCookie
          } else if (
            nameOrCookie &&
            typeof nameOrCookie === 'object' &&
            typeof nameOrCookie.name === 'string'
          ) {
            name = nameOrCookie.name
          } else {
            // This is an invalid input. Pass it through to the original method so it can error.
            return originalMethod.call(target, nameOrCookie)
          }
 
          if (!declaredNames.has(name)) {
            trackMissingSampleErrorAndThrow(
              createMissingCookieSampleError(route, name)
            )
          }
          return originalMethod.call(target, name)
        }
        return wrappedMethod
      }
 
      // TODO(instant-validation-build): what should getAll do?
      // Maybe we should only allow it if there's an array (possibly empty?)
 
      return Reflect.get(target, prop, receiver)
    },
  })
}

Sources: packages/next/src/server/app-render/instant-validation/instant-samples.ts:80-145

Caution

Defining cookies via a manual "cookie" header inside unstable_samples is explicitly rejected. Attempting to pass a "cookie" header throws an InstantValidationError, requiring the use of the structured cookies array instead.

Sources: packages/next/src/server/app-render/instant-validation/instant-samples.ts:171-176

Client Validation Instruments

Client-side parameter and search parameter access is instrumented through wrapper utilities that inspect the active work unit store. When the store type is 'validation-client', functions such as instrumentParamsForClientValidation and instrumentSearchParamsForClientValidation extract declared keys from validationSamples and wrap the underlying parameters in exhaustive proxies.

export function instrumentParamsForClientValidation<TPArams extends Params>(
  underlyingParams: TPArams
): TPArams {
  const workStore = workAsyncStorage.getStore()
  const workUnitStore = workUnitAsyncStorage.getStore()
  if (workStore && workUnitStore) {
    switch (workUnitStore.type) {
      case 'validation-client': {
        if (workUnitStore.validationSamples) {
          const declaredKeys = new Set(
            Object.keys(workUnitStore.validationSamples.params ?? {})
          )
          return createExhaustiveParamsProxy(
            underlyingParams,
            declaredKeys,
            workStore.route
          )
        }
        break
      }
      case 'prerender-runtime':
      case 'prerender-client':
      case 'prerender-legacy':
      case 'prerender':
      case 'cache':
      case 'request':
      case 'private-cache':
      case 'unstable-cache':
      case 'build-time-generator':
        break
      default:
        workUnitStore satisfies never
    }
  }
  return underlyingParams
}

Sources: packages/next/src/client/components/instant-samples.ts:12-47

Instrument FunctionUnderlying TypeTarget Store TypePurpose
instrumentParamsForClientValidationTPAramsvalidation-clientWraps route parameters in an exhaustive validation proxy
expectCompleteParamsInClientValidationstring (expression)validation-clientAsserts that all fallback route parameters are present
instrumentSearchParamsForClientValidationReadonlyURLSearchParamsvalidation-clientWraps search parameters in an exhaustive validation proxy

Sources: packages/next/src/client/components/instant-samples.ts:12-122

Note

When client validation is executed outside of the development browser environment, instant-samples.browser.ts exports stub bindings set to undefined, disabling parameter instrumentation during standard browser bundle execution.

Sources: packages/next/src/client/components/instant-samples.browser.ts:1-3

Staged Stream Generation and Transport

Staged Stream Splitting and Transport Trees

Staged Segment Data Collection

collectStagedSegmentData splits a unified prefetched stream into separate staged data chunks for individual route segments. The function determines which rendering stages to collect based on the provided ValidationPrefetchKind parameter. For a static app shell prefetch, it captures shells, prefetch APIs, and static prefetch stages; for static pages, it captures static prefetches; and for dynamic pages, it includes dynamic render stages.

export async function collectStagedSegmentData(
  prefetchKind: ValidationPrefetchKind,
  ComponentMod: FlightComponentMod,
  renderFlightStream: RenderToFlightStream,
  fullPageChunks: StageChunks,
  fullPageDebugChunks: Uint8Array[] | null,
  startTime: number,
  stageEndTimes: StageEndTimes,
  clientReferenceManifest: ClientReferenceManifest,
  createDebugChannel: () => DebugChannelPair | undefined
): Promise<{ cache: SegmentCache; payload: InitialRSCPayload }> {
  const cache = createSegmentCache()
 
  let partialStages: SegmentStage[]
  switch (prefetchKind) {
    case ValidationPrefetchKind.StaticAppShell: {
      partialStages = [
        RenderStage.ShellStatic,
        RenderStage.PrefetchStatic_prefetchApi,
        RenderStage.PrefetchStatic,
        RenderStage.NavigationStatic, // TODO(cache-stages): only if needed
        RenderStage.PrefetchRuntime,
      ]
      break
    }
    case ValidationPrefetchKind.RuntimeAppShell: {
      partialStages = [
        RenderStage.ShellRuntime,
        RenderStage.PrefetchRuntime_prefetchApi,
        RenderStage.PrefetchRuntime,
        RenderStage.NavigationRuntime, // TODO(cache-stages): only if needed

Sources: packages/next/src/server/app-render/instant-validation/instant-validation.tsx:189-219

Validation Prefetch KindCaptured Render Stages
ValidationPrefetchKind.StaticAppShellRenderStage.ShellStatic, RenderStage.PrefetchStatic_prefetchApi, RenderStage.PrefetchStatic
ValidationPrefetchKind.StaticPageRenderStage.PrefetchStatic
ValidationPrefetchKind.DynamicPageRenderStage.Dynamic

Sources: packages/next/src/server/app-render/instant-validation/instant-validation.tsx:202-218

Note

The collected chunks are stored within a segmented cache structure mapped by SegmentPath keys, allowing the validation engine to inspect individual RSC payloads and flight data streams per route segment depth.

Sources: packages/next/src/server/app-render/instant-validation/instant-validation.tsx:200-201

Route Segment Path Discovery

The validation pipeline traverses root seed data segments by parsing the initial RSC payload's transport tree nodes via traverseRootSeedDataSegments. Each transport node's segment is stringified into a hierarchical SegmentPath, combining parent paths with parallel route keys and encoded segment identifiers.

function traverseRootSeedDataSegments(
  initialRSCPayload: InitialRSCPayload,
  processSegment: (segmentPath: SegmentPath, data: TransportSegmentData) => void
) {
  const rootNode = initialRSCPayload.t.t
  const rootPath = stringifySegment(transportSegmentToSegment(rootNode.s))
  return traverseTransportNodeSegments(rootPath, rootNode, processSegment)
}
 
function traverseTransportNodeSegments(
  path: SegmentPath,
  node: FullTransportNode,
  processSegment: (segmentPath: SegmentPath, data: TransportSegmentData) => void
): void {
  processSegment(path, node.d)
 
  const children = node.c
  if (children === undefined) {
    return
  }
  for (const [parallelRouteKey, childNode] of children) {
    const childPath = createChildSegmentPath(
      path,
      parallelRouteKey,
      transportSegmentToSegment(childNode.s)
    )
 
    traverseTransportNodeSegments(childPath, childNode, processSegment)
  }
}
 
function createChildSegmentPath(
  parentPath: SegmentPath,
  parallelRouteKey: string,
  segment: Segment
): SegmentPath {
  const parallelRoutePrefix =
    parallelRouteKey === 'children'
      ? ''
      : `@${encodeURIComponent(parallelRouteKey)}/`
  return `${parentPath}/${parallelRoutePrefix}${stringifySegment(segment)}` as SegmentPath
}
 
function stringifySegment(segment: Segment): SegmentPath {
  return (
    typeof segment === 'string'
      ? encodeURIComponent(segment)
      : encodeURIComponent(segment[0]) + '|' + segment[1] + '|' + segment[2]
  ) as SegmentPath
}

Sources: packages/next/src/server/app-render/instant-validation/instant-validation.tsx:114-163

discoverValidationDepths walks loader trees to calculate group depths by URL depth across parallel slots. It tracks how many route group segments exist between URL-consuming segments, ensuring that shallow slots correctly remain in the dynamic stage when they run out of group layers before reaching validation boundaries.

export function discoverValidationDepths(loaderTree: LoaderTree): number[] {
  const groupDepthsByUrlDepth: number[] = []
 
  function recordGroupDepth(urlDepth: number, groupDepth: number): void {
    while (groupDepthsByUrlDepth.length <= urlDepth) {
      groupDepthsByUrlDepth.push(0)
    }
    if (groupDepth > groupDepthsByUrlDepth[urlDepth]) {
      groupDepthsByUrlDepth[urlDepth] = groupDepth
    }
  }
 
  // urlDepth tracks the index of the current URL-consuming segment.
  // Groups accumulate at the same index. When the next URL segment
  // is reached, it increments the index and resets the group counter.
  // We start at -1 so the root segment '' increments to 0.
  function walk(tree: LoaderTree, urlDepth: number, groupDepth: number): void {
    const segment = tree[0]
    const { parallelRoutes } = parseLoaderTree(tree)
    const consumesDepth = segmentConsumesURLDepth(segment)
 
    let nextUrlDepth = urlDepth
    let nextGroupDepth = groupDepth
    if (consumesDepth) {
      nextUrlDepth = urlDepth + 1
      nextGroupDepth = 0
      recordGroupDepth(nextUrlDepth, 0)
    } else if (
      typeof segment === 'string' &&
      isGroupSegment(segment) &&
      segment !== '(__SLOT__)'
    ) {
      // Count real route groups but not the synthetic '(__SLOT__)' segment

Sources: packages/next/src/server/app-render/instant-validation/instant-validation.tsx:939-971

Development Worker and Error Delivery

Snapshotting Request Contexts

The dev validation worker executes off the main thread by receiving a serializable payload built via buildDevValidationSnapshot. This function inspects the settled AppRenderContext, instant inputs, static inputs, prefetch mode, fallback route params, and render error flags to construct a plain data structure containing headers, query parameters, URL pathnames, drafts, HMR refresh metadata, and render options.

export async function buildDevValidationSnapshot(
  ctx: AppRenderContext,
  instantInputs: ResolvedValidationInputs | null,
  staticInputs: ResolvedValidationInputs,
  prefetchMode: PrefetchingMode,
  fallbackRouteParams: OpaqueFallbackRouteParams | null,
  devRenderDidError: boolean
): Promise<DevValidationSnapshot> {

Sources: packages/next/src/server/app-render/dev-validation-worker-snapshot.ts:20-27

return {
  page: ctx.workStore.page,
  route: ctx.workStore.route,
  requestId: ctx.requestId,
  responseFinished,
  prefetchMode,
  devRenderDidError,
  nonce: ctx.nonce,
  query: ctx.query,
  request: {
    headers: [...requestStore.headers.entries()],
    urlPathname: requestStore.url.pathname,
    urlSearch: requestStore.url.search,
    rootParams: requestStore.rootParams,
    isDraftMode: requestStore.draftMode.isEnabled,
    isHmrRefresh: requestStore.isHmrRefresh ?? false,
    hmrRefreshHash: requestStore.hmrRefreshHash,
    requestStartTime: ctx.workStore.requestStartTime,
  },
  interpolatedParams: ctx.interpolatedParams,
  requestFallbackRouteParams: ctx.fallbackRouteParams,
  fallbackRouteParams,
  optimisticRouting: ctx.renderOpts.experimental.optimisticRouting,
  forceStatic: ctx.workStore.forceStatic,
  validationLevel: ctx.workStore.validationLevel,
  implicitTags: ctx.implicitTags.tags,
  additionalClientReferenceManifestPages: ctx.workStore
    .additionalClientReferenceManifestPages
    ? [...ctx.workStore.additionalClientReferenceManifestPages]
    : [],
  reactBrowserBailout: ctx.renderOpts.experimental.reactBrowserBailout,
  isDebugChannelEnabled: !!ctx.renderOpts.setReactDebugChannel,
  renderOpts: {
    images: ctx.renderOpts.images,
    allowEmptyStaticShell: ctx.renderOpts.allowEmptyStaticShell,
    partialPrefetching: ctx.renderOpts.partialPrefetching,
  },
  instantInputs: instantInputs
    ? await toSerializedInputs(instantInputs)
    : null,
  staticInputs: await toSerializedInputs(staticInputs),
}

Sources: packages/next/src/server/app-render/dev-validation-worker-snapshot.ts:61-102

Note

Debug channel streams are drained exactly once into chunk arrays during snapshot creation because instant and static inputs share the same underlying stream instance.

Sources: packages/next/src/server/app-render/dev-validation-worker-snapshot.ts:32-37

Validation Worker Execution

runDevValidation installs SWC native bindings, code-frame support, and a disk source map lookup linked to distDir. It loads the target route's compiled module using loadComponents and registers additional client reference manifests so cross-page client references resolve correctly inside the worker isolate.

export async function runDevValidation(
  message: DevValidationWorkerMessage,
  abortBuffer: SharedArrayBuffer
): Promise<DevValidationWorkerResult> {
  // Load the native SWC bindings and wire the code-frame renderer so the errors
  // logged below render with a source-mapped code frame, matching the
  // in-process dev output (the E2E tests snapshot the CLI text between the
  // validation markers). The `build/swc` graph these pull in is bundled as a
  // runtime external (see `next-runtime.webpack-config.js`), so it resolves
  // from the installed `next/dist` tree rather than being compiled into this
  // worker bundle, the same way the unbundled build worker loads it.
  await installBindings()
  installCodeFrameSupport()
  setBundlerFindSourceMapImplementation(
    createDiskSourceMapLookup(message.distDir)
  )
  setHttpClientAndAgentOptions({
    httpAgentOptions: message.nextConfigSerializable.httpAgentOptions,
  })
 
  // Populates the manifests singleton for the route via `setManifestsSingleton`
  // inside `loadComponents`, exactly as a real request does. The pool tears the
  // worker down on HMR / route recompile so the next validation reloads from a
  // clean require cache.
  const { ComponentMod } = await loadComponents<AppPageModule>({
    distDir: message.distDir,
    page: message.page,
    isAppPath: true,
    isDev: true,
    sriEnabled: false,
    needsManifestsForLegacyReasons: true,
  })
 
  await registerAdditionalClientReferenceManifests(
    message.distDir,
    message.additionalClientReferenceManifestPages
  )

Sources: packages/next/src/server/dev/dev-validation-worker.ts:309-345

function createDiskSourceMapLookup(
  distDir: string
): (sourceURL: string) => ModernSourceMapPayload | undefined {
  // The frames carry resolved paths, so compare against the resolved `distDir`
  // to keep the containment check meaningful when the project sits behind a
  // symlink.
  let canonicalDistDir = distDir
  try {
    canonicalDistDir = realpathSync(distDir)
  } catch {}
 
  const payloads = new Map<string, ModernSourceMapPayload | undefined>()
 

Sources: packages/next/src/server/dev/dev-validation-worker.ts:53-65

The worker delegates the validation pass by invoking ComponentMod.routeModule.runValidationInDev, passing the component module, message payload, and supersede abort signal. If execution yields errors or completes successfully, the worker encodes them into RSC Flight format before forwarding them to the main thread.

// Crossing into the app-page bundle: the entire validation runs there, so
// the client prerenders use the same React the user's client components
// resolve through `ComponentMod`.
const validationErrors = await ComponentMod.routeModule.runValidationInDev(
  ComponentMod,
  message,
  signal
)
 

Sources: packages/next/src/server/dev/dev-validation-worker.ts:369-377

Warning

If a supersede signal aborts or validation errors are undefined, runDevValidation immediately returns null to discard superseded validation jobs.

Sources: packages/next/src/server/dev/dev-validation-worker.ts:378-380

Surfacing Diagnostics to the Overlay

Diagnostics and runtime errors discovered during validation are formatted and delivered to the development overlay through specific error container layouts. The dev overlay classifies errors into variants such as metadata, viewport, sync-io, sync-io-client, and unrendered-segment, rendering them within an ErrorOverlayLayout paired with an InstantHeaderExplanation.

case 'dynamic-viewport': {
  switch (errorDetails.variant) {
    case 'link':
      errorMessage = (
        <>
          Next.js encountered URL data in <code>generateViewport()</code>.
        </>
      )
      break
    case 'runtime':
      errorMessage = (
        <>
          Next.js encountered runtime data in{' '}
          <code>generateViewport()</code>.
        </>
      )
      break
    case 'navigation':
      errorMessage = (
        <>
          Next.js encountered <code>navigation()</code> in{' '}
          <code>generateViewport()</code>.
        </>
      )
      break
    case 'prefetch':
      errorMessage = (
        <>
          Next.js encountered <code>prefetch()</code> in{' '}
          <code>generateViewport()</code>.
        </>
      )
      break
    case 'dynamic':
      errorMessage = (
        <>
          Next.js encountered uncached data in{' '}
          <code>generateViewport()</code>.
        </>
      )
      break
    default:
      errorMessage = errorDetails.variant satisfies never
  }

Sources: packages/next/src/next-devtools/dev-overlay/container/errors.tsx:1205-1248

case 'unrendered-segment':
  return (
    <ErrorOverlayLayout
      errorType={errorType}
      errorMessage="Next.js could not validate that a segment in your UI has instant navigation."
      headerChildren={
        <InstantHeaderExplanation kind="unrendered-segment" />
      }
      renderTabBar={renderTabBar}
      canGoPrevious={canGoPrevious}
      canGoNext={canGoNext}
      onPrevious={handlePrevious}
      onNext={handleNext}
      onClose={isServerError ? undefined : onClose}
      debugInfo={debugInfo}
      error={error}
      runtimeErrors={activeErrors}
      activeIdx={activeIdx}
      setActiveIndex={setActiveIndex}
      dialogResizerRef={dialogResizerRef}
      generateErrorInfo={generateErrorInfo}
      {...props}
    >
      <UnrenderedSegmentInfo
        route={errorDetails.route}

Sources: packages/next/src/next-devtools/dev-overlay/container/errors.tsx:1380-1404

Was this page helpful?