vitejs/vite · error · Error

Cannot deep clone non-plain object

Error message

Cannot deep clone non-plain object

What it means

Thrown by deepClone() for a value that is an object (typeof 'object', non-null) but is not an array, not a plain {Object} (Object.prototype.toString !== '[object Object]'), not a function, and not a RegExp. deepClone only knows how to structurally copy plain data; instances of Date, Map, Set, Error, class instances, etc. are rejected rather than silently mishandled.

Solutions

  1. Convert non-plain values to plain equivalents before cloning (Date -> ISO string / number, Map -> plain object, Set -> array).
  2. Avoid putting class instances or built-in non-plain objects into config that Vite will deep-clone.
  3. If you call deepClone yourself, restrict inputs to JSON-shaped data (objects, arrays, primitives, RegExp).

Example fix

// before
const cfg = { lastBuilt: new Date() }
deepClone(cfg) // throws
// after
const cfg = { lastBuilt: Date.now() }
deepClone(cfg) // ok
Defensive patterns

Strategy: type-guard

Validate before calling

function isDeepCloneable(v: unknown): boolean {
  if (v == null || typeof v !== 'object') return true
  if (Array.isArray(v)) return v.every(isDeepCloneable)
  if (Object.prototype.toString.call(v) === '[object Object]')
    return Object.values(v).every(isDeepCloneable)
  return v instanceof RegExp || typeof v === 'function'
}

Type guard

function isPlainCloneable(v: unknown): boolean {
  if (v == null) return true
  if (typeof v !== 'object') return true
  if (Array.isArray(v)) return true
  if (Object.prototype.toString.call(v) === '[object Object]') return true
  if (v instanceof RegExp) return true
  return false
}

Prevention

When it happens

Trigger: Passing a config/value containing a Date, Map, Set, Error, Promise, ArrayBuffer, TypedArray, URL, or any class instance to deepClone(). The fallthrough branch at utils.ts:1230-1232 catches all other object types.

Common situations: deepClone is used internally when cloning resolved config / user options; passing a non-serializable value (e.g. a Date in a plugin option, a Map for aliases, a class instance) triggers it. Calling deepClone directly on plugin state that holds instances.

Related errors


AI-assisted analysis of vitejs/vite@b4d66fee14 (2026-08-11). Data as JSON: /api/errors/1473b1ba0c630b81. Report an issue: GitHub.

Appendix: source

Thrown at packages/vite/src/node/utils.ts:1231

export function deepClone<T>(value: T): DeepWritable<T> {
  if (Array.isArray(value)) {
    return value.map((v) => deepClone(v)) as DeepWritable<T>
  }
  if (isObject(value)) {
    const cloned: Record<string, any> = {}
    for (const key in value) {
      cloned[key] = deepClone(value[key])
    }
    return cloned as DeepWritable<T>
  }
  if (typeof value === 'function') {
    return value as DeepWritable<T>
  }
  if (value instanceof RegExp) {
    return new RegExp(value) as DeepWritable<T>
  }
  if (typeof value === 'object' && value != null) {
    throw new Error('Cannot deep clone non-plain object')
  }
  return value as DeepWritable<T>
}

type MaybeFallback<D, V> = undefined extends V ? Exclude<V, undefined> | D : V

type MergeWithDefaultsResult<D, V> =
  Equal<D, undefined> extends true
    ? V
    : D extends Function | Array<any>
      ? MaybeFallback<D, V>
      : V extends Function | Array<any>
        ? MaybeFallback<D, V>
        : D extends Record<string, any>
          ? V extends Record<string, any>
            ? {
                [K in keyof D | keyof V]: K extends keyof D
                  ? K extends keyof V

View on GitHub (pinned to b4d66fee14)