{"record":{"id":"f507b854842655fc","repo":"remix-run/remix","slug":"database-transaction-and-rollback-both-failed","errorCode":null,"errorMessage":"Database transaction and rollback both failed","messagePattern":"Database transaction and rollback both failed","errorType":"exception","errorClass":"AggregateError","httpStatus":null,"severity":"critical","filePath":"packages/data-table/src/lib/database.ts","lineNumber":916,"sourceCode":"    if (!this.#token) {\n      let token = await this.#driver.beginTransaction(options)\n      let tx = Database.#createInternalDatabase(\n        this.#driver,\n        { now: this.#now },\n        {\n          token,\n          savepointCounter: this.#savepointCounter,\n        },\n      )\n\n      let result: result\n      try {\n        result = await callback(tx)\n      } catch (error) {\n        try {\n          await this.#driver.rollbackTransaction(token)\n        } catch (rollbackError) {\n          throw new AggregateError(\n            [error, rollbackError],\n            'Database transaction and rollback both failed',\n            { cause: error },\n          )\n        }\n        throw error\n      }\n\n      await this.#driver.commitTransaction(token)\n      return result\n    }\n\n    if (!this.capabilities.savepoints) {\n      throw new DataTableQueryError('Nested transactions require database savepoint support')\n    }\n\n    let savepointName = 'sp_' + String(this.#savepointCounter.value)\n    this.#savepointCounter.value += 1","sourceCodeStart":898,"sourceCodeEnd":934,"githubUrl":"https://github.com/remix-run/remix/blob/9696913134be3a4423513d2775f7b31d6917c049/packages/data-table/src/lib/database.ts#L898-L934","documentation":"Inside transaction(), if the callback throws, the driver's rollbackTransaction is attempted. If the rollback itself also throws, both errors are bundled into an AggregateError so the original failure (also set as cause) is not lost. This error means the connection/transaction is in a broken state (often the connection died mid-transaction).","triggerScenarios":"A transaction callback throws AND the ROLLBACK fails — connection dropped (SQLite file deleted, network DB disconnected), the transaction was already auto-rolled-back by the server, or the driver handle is closed.","commonSituations":"Database file removed or locked during a transaction; connection timeouts; nested error handling where the first error killed the session.","solutions":["Inspect error.errors[0] (the callback error) and error.errors[1] (the rollback error) to find the root cause; the rollback error usually reveals the connection problem.","Ensure the database file/connection is available and not exclusively locked; verify close/remove doesn't race transactions.","Make the operation idempotent and retry the whole transaction on a fresh connection if the DB was temporarily unavailable."],"exampleFix":"// before\ntry { await db.transaction(cb) } catch (e) { console.log(e.message) }\n\n// after\ntry {\n  await db.transaction(cb)\n} catch (error) {\n  for (let inner of error.errors ?? [error]) console.error(inner)\n}","handlingStrategy":"retry","validationCode":"// Probe the connection before starting a transaction\nawait db.hasTable('some_table') // throws early if closed/unavailable\nawait db.transaction(cb)","typeGuard":null,"tryCatchPattern":"try {\n  await db.transaction(cb)\n} catch (error) {\n  if (error instanceof AggregateError) {\n    const [callbackError, rollbackError] = error.errors\n    // log both; retry once if rollbackError indicates a dead connection\n  }\n}","preventionTips":["Never close/remove the database while transactions are in flight.","Make transaction callbacks idempotent to allow safe retries."],"tags":["data-table","transactions","rollback","aggregate-error"],"backgroundTag":"transaction-rollback-failed","analyzedSha":"9696913134be3a4423513d2775f7b31d6917c049","analyzedAt":"2026-08-27T19:55:01.024Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}