Title: sqlite: backup() rejects with "not an error" when its first step returns SQLITE_BUSY or SQLITE_LOCKED · Issue #66760 · nodejs/node · GitHub
Open Graph Title: sqlite: backup() rejects with "not an error" when its first step returns SQLITE_BUSY or SQLITE_LOCKED · Issue #66760 · nodejs/node
X Title: sqlite: backup() rejects with "not an error" when its first step returns SQLITE_BUSY or SQLITE_LOCKED · Issue #66760 · nodejs/node
Description: Version v22.22.1, v24.21.0 Platform Darwin 27.0.0 arm64 Subsystem sqlite What steps will reproduce the bug? Run this script as node repro.mjs, then as node repro.mjs --no-overlap: import { DatabaseSync, backup } from "node:sqlite"; impor...
Open Graph Description: Version v22.22.1, v24.21.0 Platform Darwin 27.0.0 arm64 Subsystem sqlite What steps will reproduce the bug? Run this script as node repro.mjs, then as node repro.mjs --no-overlap: import { Database...
X Description: Version v22.22.1, v24.21.0 Platform Darwin 27.0.0 arm64 Subsystem sqlite What steps will reproduce the bug? Run this script as node repro.mjs, then as node repro.mjs --no-overlap: import { Database...
Opengraph URL: https://github.com/nodejs/node/issues/66760
X: @github
Domain: github.com
{"@context":"https://schema.org","@type":"DiscussionForumPosting","headline":"sqlite: backup() rejects with \"not an error\" when its first step returns SQLITE_BUSY or SQLITE_LOCKED","articleBody":"### Version\n\nv22.22.1, v24.21.0\n\n### Platform\n\n```text\nDarwin 27.0.0 arm64\n```\n\n### Subsystem\n\nsqlite\n\n### What steps will reproduce the bug?\n\nRun this script as `node repro.mjs`, then as `node repro.mjs --no-overlap`:\n\n```js\nimport { DatabaseSync, backup } from \"node:sqlite\";\nimport fs from \"node:fs\";\nimport os from \"node:os\";\nimport path from \"node:path\";\n\nconst dir = fs.mkdtempSync(path.join(os.tmpdir(), \"sqlite-backup-\"));\nconst db = new DatabaseSync(path.join(dir, \"src.db\"));\ndb.exec(\"PRAGMA journal_mode = WAL; CREATE TABLE t (x BLOB)\");\nconst insert = db.prepare(\"INSERT INTO t VALUES (randomblob(4000))\");\nfor (let i = 0; i \u003c 300; i++) insert.run();\n\nconst overlap = process.argv[2] !== \"--no-overlap\";\nconst results = {};\nfor (let i = 0; i \u003c 20; i++) {\n let job;\n if (overlap) {\n // Keep a write transaction open briefly so the backup worker can attempt its first step.\n db.exec(\"BEGIN IMMEDIATE\");\n insert.run();\n job = backup(db, path.join(dir, `copy-${i}.db`));\n const until = Date.now() + 30;\n while (Date.now() \u003c until);\n db.exec(\"COMMIT\");\n } else {\n job = backup(db, path.join(dir, `copy-${i}.db`));\n }\n try {\n await job;\n results.ok = (results.ok ?? 0) + 1;\n } catch (e) {\n const key = `${e.code} errcode=${e.errcode} message=${JSON.stringify(e.message)}`;\n results[key] = (results[key] ?? 0) + 1;\n }\n}\nconsole.log(process.version, overlap ? \"overlap\" : \"no overlap\", results);\ndb.close();\nfs.rmSync(dir, { recursive: true, force: true });\n```\n\n### How often does it reproduce? Is there a required condition?\n\nIn my tests on each version above, all 20 backups failed with overlap enabled, and all 20 succeeded with `--no-overlap`. The reproducer holds a write transaction open on the same source connection for 30 ms after starting the backup. Reproduction depends on the worker attempting its first step during that interval.\n\n### What is the expected behavior? Why is that the expected behavior?\n\nThe backup should reschedule a first step that returns `SQLITE_BUSY` or `SQLITE_LOCKED`, just as it reschedules later steps when pages remain, allowing it to finish after the write transaction commits. `SQLITE_BUSY` and `SQLITE_LOCKED` from `sqlite3_backup_step()` are retryable according to the SQLite documentation. If Node does reject, the error should carry the real result code (5 or 6), not 0.\n\n### What do you see instead?\n\n```text\nv22.22.1 overlap { 'ERR_SQLITE_ERROR errcode=0 message=\"not an error\"': 20 }\nv22.22.1 no overlap { ok: 20 }\nv24.21.0 overlap { 'ERR_SQLITE_ERROR errcode=0 message=\"not an error\"': 20 }\nv24.21.0 no overlap { ok: 20 }\n```\n\nThe promise rejects with `ERR_SQLITE_ERROR`, `errcode: 0` and the message \"not an error\".\n\n### Additional information\n\nThe cause appears to be in `BackupJob::AfterThreadPoolWork` in `src/node_sqlite.cc` (the same logic is on `main`):\n\n1. A step result of `SQLITE_BUSY` or `SQLITE_LOCKED` is not treated as an error at first.\n2. The job is rescheduled only when `sqlite3_backup_remaining()` is non-zero. The count starts at 0 and can remain 0 when the first step returns `SQLITE_BUSY` or `SQLITE_LOCKED` before updating it.\n3. So when the first step returns `SQLITE_BUSY` or `SQLITE_LOCKED`, the job is not rescheduled. Because the status is not `SQLITE_DONE`, `HandleBackupError(resolver)` runs. It builds the error from the destination connection, which has no error, so the result is errcode 0, \"not an error\".\n\nA possible fix: reschedule the work whenever the step returned `SQLITE_BUSY` or `SQLITE_LOCKED`, whatever the remaining page count. If the final `backup_status_ != SQLITE_DONE` rejection path is retained, pass `backup_status_` to `HandleBackupError` there, as the earlier fatal-error branch already does.\n\nA possible workaround for this failure is to retry the backup after the overlapping write transaction ends, using a bounded retry policy and checking for both `code === 'ERR_SQLITE_ERROR'` and `errcode === 0`.\n","author":{"url":"https://github.com/EminUZUN","@type":"Person","name":"EminUZUN"},"datePublished":"2026-10-15T13:34:17Z","url":"https://github.com/nodejs/node/issues/66760"}
| react-profiling | 0 |
| react-import-map | react,react-dom,react-dom/client,react-dom/profiling,react-is,react-reconciler,react/compiler-runtime,react/jsx-dev-runtime,react/jsx-runtime,scheduler@d5750093facf |
| route-pattern | /_view_fragments/issues/show/:user_id/:repository/:id/issue_layout(.:format) |
| route-controller | voltron_issues_fragments |
| route-action | issue_layout |
| fetch-nonce | v2:e2bd7d27-a3c7-4c7c-d698-b2a6db9c814c |
| current-catalog-service-hash | 81bb79d38c15960b92d99bca9288a9108c7a47b18f2423d0f6438c5b7bcd2114 |
| request-id | E226:1E45AC:5E1B01:7A49CF:6AED2AC3 |
| html-safe-nonce | f6588b2cd169f733c3d5eb3ff09e967a8f3c3a3a321cc55f8d6fa850059b2891 |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiJFMjI2OjFFNDVBQzo1RTFCMDE6N0E0OUNGOjZBRUQyQUMzIiwidmlzaXRvcl9pZCI6IjU1MjY3Mzg5OTAyOTY1NDE4OTEiLCJyZWdpb25fZWRnZSI6ImlhZCIsInJlZ2lvbl9yZW5kZXIiOiJpYWQifQ== |
| visitor-hmac | d73c023c3c88de55bdc4c748b17d2f172dca820361eef1219adf573d292415cf |
| hovercard-subject-tag | issue:5867145870 |
| github-keyboard-shortcuts | repository,issues,copilot |
| google-site-verification | Apib7-x98H0j5cPqHWwSMm6dNU4GmODRoqxLiDzdx9I |
| octolytics-url | https://collector.github.com/github/collect |
| analytics-location | / |
| fb:app_id | 1401488693436528 |
| apple-itunes-app | app-id=1477376905, app-argument=https://github.com/_view_fragments/issues/show/nodejs/node/66760/issue_layout |
| twitter:image | https://opengraph.githubassets.com/a8c48cd71d7271c1e3bd5daba99edf6ebe672c882245e008b6d862c4988bc0c2/nodejs/node/issues/66760 |
| twitter:card | summary_large_image |
| og:image | https://opengraph.githubassets.com/a8c48cd71d7271c1e3bd5daba99edf6ebe672c882245e008b6d862c4988bc0c2/nodejs/node/issues/66760 |
| og:image:alt | Version v22.22.1, v24.21.0 Platform Darwin 27.0.0 arm64 Subsystem sqlite What steps will reproduce the bug? Run this script as node repro.mjs, then as node repro.mjs --no-overlap: import { Database... |
| og:image:width | 1200 |
| og:image:height | 600 |
| og:site_name | GitHub |
| og:type | object |
| og:author:username | EminUZUN |
| hostname | github.com |
| expected-hostname | github.com |
| None | 9d95365dda0576efe9b71f45807595e396bd0ca9286fba5367a05048e6ddf8f2 |
| turbo-cache-control | no-preview |
| go-import | github.com/nodejs/node git https://github.com/nodejs/node.git |
| octolytics-dimension-user_id | 9950313 |
| octolytics-dimension-user_login | nodejs |
| octolytics-dimension-repository_id | 27193779 |
| octolytics-dimension-repository_nwo | nodejs/node |
| octolytics-dimension-repository_public | true |
| octolytics-dimension-repository_is_fork | false |
| octolytics-dimension-repository_network_root_id | 27193779 |
| octolytics-dimension-repository_network_root_nwo | nodejs/node |
| turbo-body-classes | logged-out env-production page-responsive |
| disable-turbo | false |
| browser-stats-url | https://api.github.com/_private/browser/stats |
| browser-errors-url | https://api.github.com/_private/browser/errors |
| release | 98b41fa4b5635ceb45a4c54320fced73817ff290 |
| ui-target | full |
| theme-color | #1e2327 |
| color-scheme | light dark |
| react-app-name | issues |
Links:
Viewport: width=device-width