{"id":"f1f01aea0bc0a28e","repo":"brianc/node-postgres","slug":"circular-reference-detected-while-preparing-val","errorCode":null,"errorMessage":"circular reference detected while preparing \"${val}\" for query","messagePattern":"circular reference detected while preparing \"(.+?)\" for query","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/pg/lib/utils.js","lineNumber":76,"sourceCode":"        return dateToStringUTC(val)\n      } else {\n        return dateToString(val)\n      }\n    }\n    if (Array.isArray(val)) {\n      return arrayString(val)\n    }\n\n    return prepareObject(val, seen)\n  }\n  return val.toString()\n}\n\nfunction prepareObject(val, seen) {\n  if (val && typeof val.toPostgres === 'function') {\n    seen = seen || []\n    if (seen.indexOf(val) !== -1) {\n      throw new Error('circular reference detected while preparing \"' + val + '\" for query')\n    }\n    seen.push(val)\n\n    return prepareValue(val.toPostgres(prepareValue), seen)\n  }\n  return JSON.stringify(val)\n}\n\nfunction dateToString(date) {\n  let offset = -date.getTimezoneOffset()\n\n  let year = date.getFullYear()\n  const isBCYear = year < 1\n  if (isBCYear) year = Math.abs(year) + 1 // negative years are 1 off their BC representation\n\n  let ret =\n    String(year).padStart(4, '0') +\n    '-' +","sourceCodeStart":58,"sourceCodeEnd":94,"githubUrl":"https://github.com/brianc/node-postgres/blob/c5e8c9a57bff6d9160ec5dbd5c4f4c1e4c460711/packages/pg/lib/utils.js#L58-L94","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before -- self-referential custom type\nclass Node {\n  constructor(v) { this.v = v; this.parent = null }\n  toPostgres(prepare) {\n    return prepare({ v: this.v, parent: this.parent }) // re-enters self\n  }\n}\n// after -- return a fresh acyclic shape\nclass Node {\n  toPostgres() {\n    return JSON.stringify({ v: this.v })\n  }\n}","handlingStrategy":"validation","validationCode":"function assertAcyclicToPostgres(root) {\n  const seen = new WeakSet()\n  function visit(v) {\n    if (v && typeof v === 'object') {\n      if (seen.has(v)) throw new Error('cyclic object graph passed to pg')\n      seen.add(v)\n      if (typeof v.toPostgres === 'function') {\n        // only toPostgres-bearing objects are tracked by pg itself\n        for (const k of Object.keys(v)) visit(v[k])\n      }\n      seen.delete(v)\n    }\n  }\n  visit(root)\n}\nassertAcyclicToPostgres(param)","typeGuard":"function isAcyclic(obj): boolean {\n  const seen = new WeakSet()\n  function visit(v): boolean {\n    if (v == null || typeof v !== 'object') return true\n    if (seen.has(v)) return false\n    seen.add(v)\n    const ok = Object.values(v).every(visit)\n    seen.delete(v)\n    return ok\n  }\n  return visit(obj)\n}","tryCatchPattern":"try {\n  await client.query(sql, [param])\n} catch (err) {\n  if (/circular reference detected/.test(err.message)) {\n    logger.warn('cyclic param -- falling back to JSON', { param })\n    await client.query(sql, [JSON.stringify(param)])\n  } else {\n    throw err\n  }\n}","preventionTips":["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."],"tags":["serialization","query-parameters","circular-reference"],"analyzedSha":"c5e8c9a57bff6d9160ec5dbd5c4f4c1e4c460711","analyzedAt":"2026-08-03T18:47:28.334Z","schemaVersion":2}