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
- In toPostgres, return a fresh plain object/array (never `this`) so the cycle is broken at serialization time.
- Remove the self-reference from the serialized shape explicitly.
- Use a custom JSON.stringify replacer instead of toPostgres.
- 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
- Always return a fresh acyclic object from toPostgres, never `this`.
- Validate cyclic object graphs at API boundaries with a WeakSet walk before passing to pg.
- Prefer passing flat rows to pg rather than object graphs that may reference parents.
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.