brianc/node-postgres · error · Error

circular reference detected while preparing "${val}" for que

Error message

circular reference detected while preparing "${val}" for query

What it means

prepareObject (utils.js:72) detects cycles only for objects that implement a custom `toPostgres` method. The `seen` array (line 74-78) tracks visited objects; re-encountering one throws. Plain objects without toPostgres fall back to JSON.stringify (line 82), which has its own circular error; arrays and buffers are handled earlier in prepareValue.

Source

Thrown at packages/pg/lib/utils.js:76

        return dateToStringUTC(val)
      } else {
        return dateToString(val)
      }
    }
    if (Array.isArray(val)) {
      return arrayString(val)
    }

    return prepareObject(val, seen)
  }
  return val.toString()
}

function prepareObject(val, seen) {
  if (val && typeof val.toPostgres === 'function') {
    seen = seen || []
    if (seen.indexOf(val) !== -1) {
      throw new Error('circular reference detected while preparing "' + val + '" for query')
    }
    seen.push(val)

    return prepareValue(val.toPostgres(prepareValue), seen)
  }
  return JSON.stringify(val)
}

function dateToString(date) {
  let offset = -date.getTimezoneOffset()

  let year = date.getFullYear()
  const isBCYear = year < 1
  if (isBCYear) year = Math.abs(year) + 1 // negative years are 1 off their BC representation

  let ret =
    String(year).padStart(4, '0') +
    '-' +

View on GitHub (pinned to c5e8c9a57b)

Solutions

  1. In toPostgres, return a fresh plain object/array (never `this`) so the cycle is broken at serialization time.
  2. Remove the self-reference from the serialized shape explicitly.
  3. Use a custom JSON.stringify replacer instead of toPostgres.
  4. Flatten cyclic structures into rows before passing them to pg.

Example fix

// before -- self-referential custom type
class Node {
  constructor(v) { this.v = v; this.parent = null }
  toPostgres(prepare) {
    return prepare({ v: this.v, parent: this.parent }) // re-enters self
  }
}
// after -- return a fresh acyclic shape
class Node {
  toPostgres() {
    return JSON.stringify({ v: this.v })
  }
}
Defensive patterns

Strategy: validation

Validate before calling

function assertAcyclicToPostgres(root) {
  const seen = new WeakSet()
  function visit(v) {
    if (v && typeof v === 'object') {
      if (seen.has(v)) throw new Error('cyclic object graph passed to pg')
      seen.add(v)
      if (typeof v.toPostgres === 'function') {
        // only toPostgres-bearing objects are tracked by pg itself
        for (const k of Object.keys(v)) visit(v[k])
      }
      seen.delete(v)
    }
  }
  visit(root)
}
assertAcyclicToPostgres(param)

Type guard

function isAcyclic(obj): boolean {
  const seen = new WeakSet()
  function visit(v): boolean {
    if (v == null || typeof v !== 'object') return true
    if (seen.has(v)) return false
    seen.add(v)
    const ok = Object.values(v).every(visit)
    seen.delete(v)
    return ok
  }
  return visit(obj)
}

Try / catch

try {
  await client.query(sql, [param])
} catch (err) {
  if (/circular reference detected/.test(err.message)) {
    logger.warn('cyclic param -- falling back to JSON', { param })
    await client.query(sql, [JSON.stringify(param)])
  } else {
    throw err
  }
}

Prevention

When it happens

Trigger: An object passed as a query parameter has a `toPostgres(prepareValue)` method (line 73) whose return value — after re-preparation via line 80 — references the original object, directly or transitively, causing seen.indexOf(val) at line 75 to find it.

Common situations: A custom ORM/DTO type whose toPostgres returns `this`; a linked-list/tree node whose toPostgres serializes children that point back to the parent; a GraphQL relay node; refactoring a domain object to add toPostgres without breaking cycles.


AI-assisted analysis of brianc/node-postgres@c5e8c9a57b (2026-08-03). Data as JSON: /data/errors/f1f01aea0bc0a28e.json. Report an issue: GitHub.