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
| Aspect | Details |
|---|---|
| CWE Reference | CWE-415 (Double Free) |
| Severity | HIGH (CVSS: 6.5) |
| Category | Correctness / Reliability |
Rule Details
Releasing a PostgreSQL client twice corrupts the connection pool state:
- First call: Returns client to pool ✅
- Second call: Pool thinks it received a "new" client ❌
- Result: Pool tracks phantom connections, queries timeout, memory leaks
Detection Patterns (13 Cases)
| # | Pattern | Description |
|---|---|---|
| 1 | Try + Catch | Release in try AND catch blocks |
| 2 | Catch + Try | Reversed order detection |
| 3 | Switch Fallthrough | Missing break causes double release |
| 4 | If without Else | If-release + sequential release |
| 5 | Two If Statements | Sequential ifs without guards |
| 6 | Same Block | Direct sequential releases |
| 7 | Catch + Finally | Release in catch AND finally |
| 8 | Try + Finally | Release in try AND finally |
| 9 | Finally + After | Release in finally AND after try |
| 10 | Try + After | Release in try AND after try statement |
| 11 | If + Finally | If-release (no exit) + finally release |
| 12 | Catch + After | Release in catch AND after try |
| 13 | Expression + Sequential | Ternary/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 executesError 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#clientreleaseKnown 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
Related Rules
- no-missing-client-release - Ensures release is called
- prefer-pool-query - Use
pool.query()for simple queries