{"record":{"id":"87f6dd2d00f96948","repo":"remix-run/remix","slug":"sqlite-rollback-and-connection-cleanup-both-failed","errorCode":null,"errorMessage":"SQLite rollback and connection cleanup both failed","messagePattern":"SQLite rollback and connection cleanup both failed","errorType":"exception","errorClass":"AggregateError","httpStatus":null,"severity":"critical","filePath":"packages/data-table-sqlite/src/lib/driver.ts","lineNumber":377,"sourceCode":"    } finally {\n      this.#transactions.delete(token.id)\n    }\n  }\n\n  /**\n   * Rolls back an open sqlite transaction.\n   * @param token Transaction token to roll back.\n   * @returns A promise that resolves when the transaction is rolled back.\n   */\n  async rollbackTransaction(token: TransactionToken): Promise<void> {\n    this.#assertTransaction(token)\n    try {\n      this.#database.exec('rollback')\n    } catch (rollbackError) {\n      try {\n        this.#discardUncertainConnection()\n      } catch (recoveryError) {\n        throw new AggregateError(\n          [rollbackError, recoveryError],\n          'SQLite rollback and connection cleanup both failed',\n          { cause: rollbackError },\n        )\n      }\n      throw rollbackError\n    } finally {\n      this.#transactions.delete(token.id)\n    }\n  }\n\n  /**\n   * Creates a savepoint in an open sqlite transaction.\n   * @param token Transaction token to use.\n   * @param name Savepoint name.\n   * @returns A promise that resolves when the savepoint is created.\n   */\n  async createSavepoint(token: TransactionToken, name: string): Promise<void> {","sourceCodeStart":359,"sourceCodeEnd":395,"githubUrl":"https://github.com/remix-run/remix/blob/9696913134be3a4423513d2775f7b31d6917c049/packages/data-table-sqlite/src/lib/driver.ts#L359-L395","documentation":"When ROLLBACK fails in rollbackTransaction, the driver tries to discard the uncertain connection as recovery; if that cleanup also fails it throws an AggregateError of both failures. The connection state is unknown, so the driver surfaces everything rather than masking the recovery error.","triggerScenarios":"ROLLBACK erroring due to a lock (SQLITE_BUSY), I/O error, or corrupted database, and the subsequent connection close/discard also throwing — e.g. native client in a broken state or file unmountable.","commonSituations":"Concurrent writers holding locks past busy_timeout; disk/NFS failures mid-transaction; native module (better-sqlite3/node:sqlite) crashing during close after a failed statement.","solutions":["Read AggregateError.errors — fix the primary rollback cause first (locks: WAL + busy_timeout + single writer; I/O: check disk space and mount)","Restart/reset the connection or process after this error; do not reuse the driver instance","Run PRAGMA integrity_check after recovery to verify the database file"],"exampleFix":"// single-writer + WAL avoids most busy rollbacks\nawait db.exec('pragma journal_mode = wal')\nawait db.exec('pragma busy_timeout = 5000')","handlingStrategy":"try-catch","validationCode":"await db.exec('pragma busy_timeout = 5000') // reduce busy-driven rollback failures","typeGuard":null,"tryCatchPattern":"try { await db.rollbackTransaction(tx) } catch (e) { if (e instanceof AggregateError) { /* connection uncertain: recreate driver, run integrity_check */ } throw e }","preventionTips":["Serialize writes through one connection","Use WAL mode","After this error, recreate the connection and verify integrity"],"tags":["sqlite","transaction","rollback","aggregate-error"],"backgroundTag":"sqlite-rollback-failed","analyzedSha":"9696913134be3a4423513d2775f7b31d6917c049","analyzedAt":"2026-08-27T19:55:01.024Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}