The PostgreSQL manual is unusually direct about this: When an application receives this error message, it should abort the current transaction and retry the whole transaction from the beginning. "The whole transaction" is doing a lot of work in that sentence, and it is the part that gets dropped.
TypeORM issue #9806 — "Auto Retry options on error in transactions (e.g.
Deadlock)" — has been open since February
2023.
Thirty 👍, six comments, no implementation.
Meanwhile , at 188,000 downloads a week, ships with isolation levels and seven propagation modes and no retry at all.
So the ecosystem's actual answer to "how do I use in Node" is: don't.
Use , don't think about write skew, and hope.
I spent a while building the thing that issue asks for.
The short version of what I found: the feature as literally requested cannot be built correctly, and the reason is more interesting than the feature.
The implementation everyone reaches for first Wrap the query.
It's the obvious move — the error came from a query, so retry the query: This does nothing.
Worse than nothing — it turns one clear error into a confusing one.
When PostgreSQL raises , it does not fail that statement.
It aborts the entire transaction.
The connection is now in a failed transaction state, and every subsequent statement on it — including the retry you just issued — comes back as: So burns its three attempts on a statement that is guaranteed to fail three times, then throws instead of .
You've replaced the actionable error with a meaningless one and added 150ms of sleeping to do it.
The same is true of deadlocks.
A victim's transaction is dead, not its last statement.
By the time you have an error to react to, there is no query left to re-issue.
It's worse than that: you don't know where it will fire I wrote a test asserting that under , the failure surfaces at — because that's how I understood SSI to work.
Serializable Snapshot Isolation tracks read/write dependencies and looks for a dangerous structure; I assumed the check happened at commit time.
The test passed on PostgreSQL
14.
Passed on
16.
Passed on
17.
Failed on
15.
PostgreSQL raises as soon as its machinery notices the dangerous structure.
Sometimes that's at , after every statement in your transaction has already returned successfully.
Sometimes it's at the conflicting statement itself.
It depends on the version, the plan, and the interleaving.
It is not something you can predict, and it is not something you should write code against.
The test that replaced it asserts the only thing that's actually stable: This is the general lesson, and it's the one I'd keep even if you never touch : assertions about database behaviour that you derived by reasoning are hypotheses.
Run them against the versions you actually support.
I had a clean mental model of SSI and it was wrong in a way that only a version matrix could show me.
Which pushes retry up to the transaction boundary If you can't retry the statement, retry the thing that owns the transaction.
Whatever called has to roll back, open a fresh connection and transaction, and re-run the entire callback from the top.
That's a small change in where the loop goes and an enormous change in what the API means, because now your callback runs more than once. undoes the two database writes.
It does not un-send the email, un-charge the card, or un-publish the message.
The fix is to defer everything non-transactional until after the transaction is durable: Rule of thumb: if undoing it needs more than , it belongs in a commit hook.
I suspect this constraint is a large part of why #9806 has stayed open for three and a half years.
Adding a option is an afternoon.
Adding a option that doesn't silently double-charge people requires a commit-hook mechanism, a per-attempt reset of that registry, documentation of the hazard, and a decision about what to do with in-memory state that mutated on attempt one.
It's a feature that drags a design in behind it.
Jitter is not a nice-to-have T