
ENGINEERING
AUGUST 31, 2026
node-postgres: "empty password returned by client"
5 min read
The error names the symptom and not one component that caused it. Object.assign let a parsed connection string overwrite the function minting the IAM token.
If you landed here from a search, the short version is at the bottom under **The
fix**. The rest is why it took a while to find, which is the part I think is
worth reading.
## The setup
A service connecting to RDS Postgres with IAM authentication rather than a stored
password. The appeal is obvious: no long-lived database credential anywhere in the
system, no secret to rotate, no secret to leak. The token is minted on demand and
expires in fifteen minutes.
In `node-postgres` you do that by giving the `Pool` a `password` that is a
function rather than a string. The driver calls it for each new connection, and it
returns a freshly signed token.
```js
new Pool({
host, port, database, user,
password: async () => signer.getAuthToken(),
ssl: { rejectUnauthorized: true },
})
```
That works. What did not work was the version I actually had, which built config
from a connection string first and then layered the IAM bits on top.
## The error
```
error: empty password returned by client
```
That is the whole message. It arrives on every connection attempt, immediately,
in a service that had been working the week before.
Read it literally and it is accurate: the client returned an empty password. What
it does not tell you is that a *function* was supposed to be there, that something
replaced it, or where. It describes the last moment before failure and nothing
about the cause.
## Why is this hard to find?
I did what the message suggests, in roughly this order.
**Is the token function throwing?** Logged inside it. It never ran. That is the
first genuinely useful signal, and it should have redirected me faster than it
did — a function that is never called has usually been replaced, not broken.
**Is the IAM policy wrong?** Checked `rds-db:connect`, checked the resource ARN
down to the database user. Fine. This was the expensive detour: an IAM problem and
a config problem look identical from the application, and IAM is where you expect
the trouble to be with a setup like this.
**Is the token expiring?** No — it was never being minted.
**Is it SSL?** RDS IAM auth requires TLS, and getting that wrong produces its own
confusing errors. Not this one.
Somewhere in there I stopped debugging the database and started reading how the
config object was assembled, which is where it was the whole time.
## The cause
The config was built in two steps. Parse the connection string for host, port,
database and user, then merge in the IAM-specific pieces:
```js
const parsed = parse(process.env.DATABASE_URL) // → { ..., password: '' }
const config = Object.assign(
{ password: async () => signer.getAuthToken() },
parsed, // ← this wins
)
```
`Object.assign` copies left to right, and **later sources overwrite earlier ones**.
The parsed connection string carries a `password` key. There is no password in the
URL, so its value is an empty string — and an empty string is a perfectly valid
value to assign. It silently replaced the function.
No error, no warning, no type complaint. `password` had the right name and the
wrong type, and nothing in the chain cared until Postgres was handed an empty
string and said so.
## The fix
Put the thing that must win last:
```js
const config = {
...parse(process.env.DATABASE_URL),
password: async () => signer.getAuthToken(), // last, so it survives
}
```
And assert it, because the failure mode is silent and will recur the moment
somebody adds another merge:
```js
if (typeof config.password !== 'function') {
throw new Error('IAM token function was overwritten during config assembly')
}
```
That throw is worth more than the fix. The fix stops today's bug; the assertion
turns tomorrow's version of it into an error message that names the actual problem.
## What I took from it
**A credential that is a function is a different kind of thing from a credential
that is a string,** and most config plumbing was written assuming strings.
Spreads, merges, `Object.assign`, defaults-then-overrides — all of it will
cheerfully swap a function for an empty string, because at the type level nothing
is wrong.
**When a callback never fires, suspect replacement before failure.** I lost the
most time to IAM policy because that is where I expected a problem to be, not
because the evidence pointed there. The evidence — a function that never ran —
was pointing at assignment from the start.
**Error messages describe the last moment, not the cause.** "Empty password
returned by client" is a true statement about the final microsecond of a chain of
events, and no part of it is about `Object.assign`. Worth remembering when the
message and the bug seem to have nothing to do with each other: that is normal,
not a sign you are missing something clever.
This came out of a multi-tenant clinical platform I designed and built — 95
TypeScript source files, 29 test files, 36 migrations. It is built and owned by
me, with no live client, so nobody's patients were affected by any of this. The
architecture, the isolation tests and two other bugs of this shape are public:
**[github.com/jaklabs/telehealth-platform-reference](https://github.com/jaklabs/telehealth-platform-reference)**
If you want to judge whether I am worth listening to before reading any further,
the **[free website check](/website-audit)** is a public unauthenticated endpoint
that takes an arbitrary URL from a stranger and drives headless Chromium at it,
with the SSRF boundary that implies. Poke at it rather than take my word for
anything.
And if you are shipping something where being wrong is expensive and you want
another pair of eyes on the parts that fail quietly, that is what I do.
[Tell me what you built](/contact).
Read More
MORE ARTICLES

Engineering
Engineering
Two ECS failures that produce no useful error
A readiness probe blocked by the task's own IAM role, and readonlyRootFilesystem silently killing ECS Exec. Both look like broken infrastructure.

Engineering
Engineering
How do you know when your AI feature is wrong?
Most teams ship an AI feature and have no answer past spot-checking. What an evaluation harness actually contains, and the failure it usually misses.

Engineering
Engineering
Don't put a model where you need a reproducible answer
Three systems where I deliberately chose regex and a lookup table over an LLM, and the two questions that decide which one a problem needs.

Engineering
Engineering
CLAUDE.md as production infrastructure
What a coding agent's context file has to contain when the repos it touches are live, and the day I found the file preventing disasters had no backup.