Skip to content

Record the cause of an OPFSCoopSyncVFS open that failed asynchronously - #357

Open
lalexdotcom wants to merge 2 commits into
rhashimoto:masterfrom
lalexdotcom:fix/coopsync-open-last-error
Open

lalexdotcom wants to merge 2 commits into
rhashimoto:masterfrom
lalexdotcom:fix/coopsync-open-last-error

Conversation

@lalexdotcom

Copy link
Copy Markdown

Summary

OPFSCoopSyncVFS cannot open a database synchronously: jOpen starts the work, returns SQLITE_BUSY, and answers the retried call. When that asynchronous phase fails, the error goes to the console and nowhere else — the retried jOpen returns SQLITE_CANTOPEN from a branch that has no error of its own, and lastError still holds whatever it held before, if anything.

The caller is then told only that the database could not be opened. It cannot tell a file held by another context — an ordinary, retryable condition — from a file that is not there.

One line changes: this.lastError = e in that catch.

Why this branch, and only this one

Every other error return of this VFS records its cause first: jOpen's synchronous catch, jDelete, jAccess, jClose, jRead, jWrite, jTruncate, jSync, jFileSize and jFileControl all assign lastError before returning their SQLITE_IOERR_* or SQLITE_CANTOPEN. The non-OK returns that do not assign it are results rather than failures — SQLITE_BUSY from jOpen and jLock, SQLITE_IOERR_SHORT_READ, and jFileControl's SQLITE_NOTFOUND.

So the asynchronous open is the single error path that reports nothing. It is also, for the same reason, the only path where xGetLastError can serve a stale message: nothing ever clears lastError, so a SQLITE_CANTOPEN raised here is reported with whatever error an earlier call happened to leave.

What it looks like

A worker holds an exclusive FileSystemSyncAccessHandle on demo — a connection elsewhere, or a worker that has just been terminated and whose handles the engine has not reclaimed yet:

jOpen('demo', …, SQLITE_OPEN_MAIN_DB)   ->  SQLITE_BUSY       // work started
  #requestAccessHandle throws NoModificationAllowedError      // console only
jOpen('demo', …)                        ->  SQLITE_CANTOPEN   // cause gone
vfs.lastError                           ->  undefined

With the change, lastError holds the NoModificationAllowedError.

What it does not change, measured rather than assumed: SQLite does not fold xGetLastError into the connection's message here. sqlite3.open_v2 on that held file rejects with unable to open database file both before and after — the generic string for SQLITE_CANTOPEN. So the VFS instance is the only place the cause exists, which is precisely why it has to be recorded there.

The test

test/vfs_open_last_error.js, wired into test/OPFSCoopSyncVFS.test.js. It holds the file from a worker of its own, then drives jOpen directly and asserts the recorded cause. Directly, because that is where the distinction lives: since #330 a failed sqlite3_open_v2 does report the connection's message, but that message is unable to open database file whatever the VFS knows.

It fails on master on the default and asyncify builds (Expected null to be truthy) and passes with the change. The file's other tests are unaffected.

It needs one line in test/test-worker.js. The VFS proxy returns only functions, so a test can call VFS methods but cannot read the state a call leaves behind; every non-function property answers undefined today, lastError included. Passing plain properties through makes it observable and changes nothing else.

Note

Written with Claude Code; the commits are co-signed.

Checklist

  • I grant to recipients of this Project distribution a perpetual,
    non-exclusive, royalty-free, irrevocable copyright license to reproduce, prepare
    derivative works of, publicly display, sublicense, and distribute this
    Contribution and such derivative works.
  • I certify that I am legally entitled to grant this license, and that this
    Contribution contains no content requiring a license from any third party.

lalexdotcom and others added 2 commits September 21, 2026 11:01
jOpen starts the work for a main database, returns SQLITE_BUSY and answers
the retried call. When the asynchronous phase failed, its error went to the
console only: the retried call returns SQLITE_CANTOPEN from a branch that
has no error of its own, so a caller cannot tell a file held by another
context from a file that is not there.

Every other error return of this VFS assigns lastError before returning.
This was the only one that did not, which also made it the only path where
xGetLastError could report an error left by an earlier call.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The test holds the database file from a worker of its own and drives jOpen
directly rather than through sqlite3_open_v2, which is the caller's
situation: there is no connection to ask sqlite3_errmsg.

It needs the test worker's VFS proxy to pass plain properties through. The
proxy returns only functions, so a test can call VFS methods but cannot read
the state a call leaves behind; every other property answers undefined.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant