brianc/node-postgres · error · Error

Release called on client which has already been released to

Error message

Release called on client which has already been released to the pool.

What it means

Thrown by the pool when client.release() is called more than once on the same checked-out client. The pool wraps each client's release function with _releaseOnce (index.js:369-380), which tracks a 'released' boolean; the second invocation calls throwOnDoubleRelease() at line 374. This protects pool integrity — returning the same client twice would corrupt the idle list and let two consumers use one connection.

Source

Thrown at packages/pg-pool/index.js:27

  return i === -1 ? undefined : list.splice(i, 1)[0]
}

class IdleItem {
  constructor(client, idleListener, timeoutId) {
    this.client = client
    this.idleListener = idleListener
    this.timeoutId = timeoutId
  }
}

class PendingItem {
  constructor(callback) {
    this.callback = callback
  }
}

function throwOnDoubleRelease() {
  throw new Error('Release called on client which has already been released to the pool.')
}

function promisify(Promise, callback) {
  if (callback) {
    return { callback: callback, result: undefined }
  }
  let rej
  let res
  const cb = function (err, client) {
    err ? rej(err) : res(client)
  }
  const result = new Promise(function (resolve, reject) {
    res = resolve
    rej = reject
  }).catch((err) => {
    // replace the stack trace that leads to `TCP.onStreamRead` with one that leads back to the
    // application that created the query
    Error.captureStackTrace(err)

View on GitHub (pinned to c5e8c9a57b)

Solutions

  1. Track release state with a boolean flag and only call release() when not already released.
  2. Prefer pool.query() over manual pool.connect()/release() so the pool handles lifecycle automatically.
  3. Ensure error and success handlers are mutually exclusive (early-return after the first release).

Example fix

// before
pool.connect(async (err, client, release) => {
  if (err) { release(err); return; }
  try {
    await client.query('SELECT 1');
    release();
  } catch (e) {
    release(e);
  }
  release(); // bug: always runs
});

// after
pool.connect(async (err, client, release) => {
  if (err) { release(err); return; }
  try {
    await client.query('SELECT 1');
    release();
  } catch (e) {
    release(e);
  }
  // no unconditional release
});
// or simpler:
pool.query('SELECT 1').then(...)
Defensive patterns

Strategy: validation

Validate before calling

function makeSafeRelease(release) {
  let released = false;
  return (err) => {
    if (released) return; // silently ignore double release
    released = true;
    release(err);
  };
}

// usage:
pool.connect((err, client, done) => {
  const release = makeSafeRelease(done);
  // use release() everywhere; double calls are no-ops
});

Try / catch

pool.connect(async (err, client, release) => {
  if (err) throw err;
  let released = false;
  const safeRelease = (e) => {
    if (released) return;
    released = true;
    release(e);
  };
  try {
    await client.query('SELECT 1');
    safeRelease();
  } catch (e) {
    safeRelease(e);
  }
});

Prevention

When it happens

Trigger: pool.connect((err, client, release) => { release(); release(); }). Also: calling client.release() in an error handler and again in the success handler of the same query. Using pool.query() internally does not trigger this (it manages release itself), but manual pool.connect() with overlapping release calls does.

Common situations: An error-handling path calls release(err) and then the normal completion path also calls release(). Async control flow (e.g., a timeout race) that releases then the query callback also releases. Wrapping pooled client usage in a helper that releases on exit but the caller also releases.


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