{"record":{"id":"9c26c4614bb6b581","repo":"remix-run/remix","slug":"nested-transaction-cleanup-failed","errorCode":null,"errorMessage":"Nested transaction cleanup failed","messagePattern":"Nested transaction cleanup failed","errorType":"exception","errorClass":"AggregateError","httpStatus":null,"severity":"critical","filePath":"packages/data-table/src/lib/database.ts","lineNumber":954,"sourceCode":"    await this.#driver.createSavepoint(this.#token, savepointName)\n\n    let result: result\n    try {\n      result = await callback(this)\n    } catch (error) {\n      let failures: unknown[] = [error]\n      try {\n        await this.#driver.rollbackToSavepoint(this.#token, savepointName)\n      } catch (rollbackError) {\n        failures.push(rollbackError)\n      }\n      try {\n        await this.#driver.releaseSavepoint(this.#token, savepointName)\n      } catch (releaseError) {\n        failures.push(releaseError)\n      }\n      if (failures.length > 1) {\n        throw new AggregateError(failures, 'Nested transaction cleanup failed', { cause: error })\n      }\n      throw error\n    }\n\n    await this.#driver.releaseSavepoint(this.#token, savepointName)\n    return result\n  }\n\n  async #executeOperation(operation: DataManipulationOperation): Promise<DataManipulationResult> {\n    try {\n      return await this.#driver.execute({\n        operation,\n        transaction: this.#token,\n      })\n    } catch (error) {\n      throw new DataTableDatabaseError('Database execution failed', {\n        cause: error,\n        metadata: {","sourceCodeStart":936,"sourceCodeEnd":972,"githubUrl":"https://github.com/remix-run/remix/blob/9696913134be3a4423513d2775f7b31d6917c049/packages/data-table/src/lib/database.ts#L936-L972","documentation":"When a nested (savepoint) transaction callback throws, the Database rolls back to the savepoint and releases it. If both the rollback-to-savepoint and the releaseSavepoint fail, the failures are collected and thrown as an AggregateError with the original error as cause — the savepoint machinery itself is broken, typically because the connection died.","triggerScenarios":"A nested transaction callback throws while the underlying connection is broken (database closed, file removed, network dropped), so both savepoint cleanup calls fail.","commonSituations":"Closing the DB or dropping the connection while nested transactions are in flight; SQLite file lock/remove during tests; long transactions hitting driver timeouts.","solutions":["Read error.errors ([rollback-to-savepoint error, release error]) with cause = the callback error; the cleanup failures usually point at a dead connection.","Ensure close()/connection teardown can't race in-flight transactions (await outstanding work first).","Retry the outer transaction on a fresh connection once the database is available again."],"exampleFix":null,"handlingStrategy":"retry","validationCode":"// Ensure the connection is alive before nested transactions\nawait db.hasTable('some_table')\nawait db.transaction(async (tx) => tx.transaction(inner))","typeGuard":null,"tryCatchPattern":"try {\n  await db.transaction(outer)\n} catch (error) {\n  if (error instanceof AggregateError && /Nested transaction cleanup/.test(error.message)) {\n    // connection-level failure; inspect error.errors and retry on a fresh db\n  }\n}","preventionTips":["Await all transactions before closing the database.","Avoid deep savepoint nesting in long-running operations."],"tags":["data-table","transactions","savepoints","aggregate-error"],"backgroundTag":"transaction-rollback-failed","analyzedSha":"9696913134be3a4423513d2775f7b31d6917c049","analyzedAt":"2026-08-27T19:55:01.024Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}