vercel/next.js · error · SerializableError

Error serializing `${path}` returned from `${method}` in "${

Error message

Error serializing `${path}` returned from `${method}` in "${page}".
Reason: Circular references cannot be expressed in JSON (references: `${visited.get(value) || '(self)'}`).

What it means

Thrown by isSerializableProps() when, during deep traversal of the props object, a value is encountered that already exists in the visited Map — i.e. a circular reference. JSON cannot express cycles, so Next.js reports the first path that closed the loop (or '(self)' if it points to its own path).

Source

Thrown at packages/next/src/lib/is-serializable-props.ts:36

export function isSerializableProps(
  page: string,
  method: string,
  input: any
): true {
  if (!isPlainObject(input)) {
    throw new SerializableError(
      page,
      method,
      '',
      `Props must be returned as a plain object from ${method}: \`{ props: { ... } }\` (received: \`${getObjectClassLabel(
        input
      )}\`).`
    )
  }

  function visit(visited: Map<any, string>, value: any, path: string) {
    if (visited.has(value)) {
      throw new SerializableError(
        page,
        method,
        path,
        `Circular references cannot be expressed in JSON (references: \`${
          visited.get(value) || '(self)'
        }\`).`
      )
    }

    visited.set(value, path)
  }

  function isSerializable(
    refs: Map<any, string>,
    value: any,
    path: string
  ): true {
    const type = typeof value

View on GitHub (pinned to 0ae8c72462)

Solutions

  1. Break the cycle before returning: map bidirectional relations to one direction only, or replace back-references with IDs.
  2. Use a safe stringify or selective picker (e.g. only expose whitelisted fields) to flatten the graph into a tree.
  3. Sanitize with JSON.parse(JSON.stringify(props)) to drop cyclic references before returning.
  4. Unit test data-fetching functions against a serialize check.

Example fix

// before
export async function getStaticProps() {
  const post = await getPost(id)
  post.author.posts = [post] // circular: post -> author -> post
  return { props: { post } }
}
// after
export async function getStaticProps() {
  const post = await getPost(id)
  return {
    props: {
      post: { ...post, author: { name: post.author.name } } // no back-ref
    },
  }
}
Defensive patterns

Strategy: validation

Validate before calling

function hasCycle(obj: unknown): boolean {
  const seen = new WeakSet()
  function visit(v: unknown): boolean {
    if (v && typeof v === 'object') {
      if (seen.has(v)) return true
      seen.add(v)
      return Object.values(v).some(visit)
    }
    return false
  }
  return visit(obj)
}
// if (hasCycle(props)) throw new Error('circular props')

Type guard

function isAcyclic(v: unknown, seen = new WeakSet()): boolean {
  if (v && typeof v === 'object') {
    if (seen.has(v)) return false
    seen.add(v)
    return Object.values(v).every((c) => isAcyclic(c, seen))
  }
  return true
}

Prevention

When it happens

Trigger: A getStaticProps/getServerSideProps return value contains an object that references itself (directly or transitively). The visit() function tracks each object/array by path; re-encountering one throws with the original path that referenced it.

Common situations: Bidirectional ORM relations (user.posts[].user); manually linking nodes in a tree; caching an object and reusing the same reference in multiple places that form a cycle; or building a graph data structure in props.

Related errors


AI-assisted analysis of vercel/next.js@0ae8c72462 (2026-08-06). Data as JSON: /api/errors/0bf9380288b40eb7. Report an issue: GitHub.