Interlace ESLint
ESLint Interlace
PostgreSQLRules

prevent-double-release

Prevents calling `client.release()` multiple times on the same client.

Keywords: double release, connection pool, CWE-415, pg, node-postgres, pool corruption

Prevents calling client.release() multiple times on the same client.

⚠️ This rule errors by default in the recommended config.

Quick Summary

AspectDetails
CWE ReferenceCWE-415 (Double Free)
SeverityHIGH (CVSS: 6.5)
CategoryCorrectness / Reliability

Rule Details

Releasing a PostgreSQL client twice corrupts the connection pool state:

  1. First call: Returns client to pool ✅
  2. Second call: Pool thinks it received a "new" client ❌
  3. Result: Pool tracks phantom connections, queries timeout, memory leaks

Detection Patterns (13 Cases)

#PatternDescription
1Try + CatchRelease in try AND catch blocks
2Catch + TryReversed order detection
3Switch FallthroughMissing break causes double release
4If without ElseIf-release + sequential release
5Two If StatementsSequential ifs without guards
6Same BlockDirect sequential releases
7Catch + FinallyRelease in catch AND finally
8Try + FinallyRelease in try AND finally
9Finally + AfterRelease in finally AND after try
10Try + AfterRelease in try AND after try statement
11If + FinallyIf-release (no exit) + finally release
12Catch + AfterRelease in catch AND after try
13Expression + SequentialTernary/short-circuit release + sequential

❌ Incorrect

// Pattern: Catch + Finally
async function query() {
  const client = await pool.connect();
  try {
    await client.query('SELECT ...');
  } catch (e) {
    client.release(); // Released on error
    throw e;
  } finally {
    client.release(); // Released again! ❌
  }
}

// Pattern: Try + Finally
async function query() {
  const client = await pool.connect();
  try {
    client.release(); // Released in try
  } finally {
    client.release(); // Released again! ❌
  }
}

// Pattern: Switch Fallthrough
async function query(type: string) {
  const client = await pool.connect();
  switch (type) {
    case 'a':
      client.release(); // Missing break!
    case 'b':
      client.release(); // Falls through ❌
      break;
  }
}

// Pattern: Expression + Sequential
async function query() {
  const client = await pool.connect();
  condition ? client.release() : null;
  client.release(); // ❌
}

// Pattern: Short-circuit
async function query() {
  const client = await pool.connect();
  shouldRelease && client.release();
  client.release(); // ❌
}

// Pattern: Destructured release
async function query() {
  const { release } = await pool.connect();
  release();
  release(); // ❌
}

✅ Correct

// Best: Single release point in finally
async function query() {
  const client = await pool.connect();
  try {
    await client.query('SELECT 1');
  } catch (e) {
    throw e; // Don't release here
  } finally {
    client.release(); // Only release point ✅
  }
}

// Guarded release pattern
async function queryWithGuard() {
  const client = await pool.connect();
  let released = false;

  const safeRelease = () => {
    if (!released) {
      released = true;
      client.release();
    }
  };

  try {
    return await client.query('SELECT 1');
  } finally {
    safeRelease();
  }
}

// Mutually exclusive branches (valid)
async function query() {
  const client = await pool.connect();
  if (condition) {
    client.release();
  } else {
    client.release();
  }
} // ✅ Only one path executes

Error Message Format

📚 Client release() called multiple times on the same object. | HIGH
   Fix: Ensure client.release() is called exactly once per acquisition, preferably in a finally block. | https://node-postgres.com/api/client#clientrelease

Known False Negatives

The following patterns are not detected due to static AST analysis limitations:

Loop Patterns

Why: Static analysis cannot determine how many times a loop will execute at runtime. The loop might run 0, 1, or N times depending on runtime conditions.

// ❌ NOT DETECTED
async function loopRelease() {
  const client = await pool.connect();
  for (let i = 0; i < 1; i++) {
    client.release();
  }
  client.release(); // Double release if loop executes!
}

Dynamic Dispatch

Why: When release() is called through array methods or callbacks, the rule cannot track which client instance is being released because the reference is indirect.

// ❌ NOT DETECTED
const clients = [await pool.connect()];
clients.forEach((c) => c.release());
clients[0].release(); // Double release!

Aliased Functions

Why: When release is assigned to a variable or bound, the rule loses the connection between the function and the original client variable.

// ❌ NOT DETECTED
const client = await pool.connect();
const rel = client.release.bind(client);
rel();
rel(); // Double release!

Callback done() Pattern

Why: The callback parameter tracking requires following the done identifier through the callback body, which has partial support.

// ⚠️ PARTIALLY DETECTED
pool.connect((err, client, done) => {
  if (err) {
    done();
    return;
  }
  done(); // May not be detected in all cases
});

Workaround: For any of these patterns, use the guarded release pattern shown in the examples above.

When Not To Use It

  • Generally, keep this rule enabled — double release is always a bug
  • If using a wrapper library that tracks release state internally

On this page