Transactions in NestJS and TypeORM without passing the EntityManager around
Transactions promise a simple guarantee: either everything commits, or nothing does. And yet, in a NestJS application with a repository layer, it is perfectly possible to run a rollback with no errors and then find a row still sitting in the database that should have disappeared with it. This is not a TypeORM or PostgreSQL bug. One of the repositories involved was never inside the transaction, because the EntityManager stopped being passed down three layers up. There was no exception, no warning, and the tests passed because that repository was mocked. This article describes how to make that class of failure impossible: the transaction opens at a single point — the controller handling the request — and repositories enlist themselves in the transaction in progress, without receiving anything as a parameter. It comes to about sixty lines built on AsyncLocalStorage . The second part is the one rarely told: three consequences of the transaction boundary, each with its fix. A network call inside the transaction holds a pooled connection and its locks for the entire wait. A failure record written in the catch is rolled back along with the very failure it was meant to document. And nesting two execute calls does not open a nested transaction but two independent ones, with the self-deadlock that allows. The problem: passing the EntityManager by hand TypeORM offers a transaction like this: await dataSource . transaction ( async ( manager ) => { await manager . getRepository ( UserModel ). save ( user ); await manager . getRepository ( UserSettingModel ). save ( settings ); }); For a small project this is the correct answer and nothing more is needed. The problem shows up once a repository layer exists. The manager is the transaction: if a repository does not use that manager, its queries run on a different connection and end up outside the transaction. Silently, with no error and no warning. The rollback simply does not revert them. So the manager has to reach the repository