SQLite ships an official browser WebAssembly build
SQLite 3.40.0 adds a beta WebAssembly build with JavaScript interfaces and browser demonstrations. Its examples show how initialization, statement ownership, and storage differ from simply opening a native database.
Compiling a SQL engine for a browser is only part of making it usable there. Application code also needs an initialization path, a JavaScript interface, and a way to manage database resources. SQLite 3.40.0, released on November 16, includes official WebAssembly support for browser execution. The release explicitly labels the build and interfaces beta, allowing minor interface changes if needed.
The useful development is the supported integration around the engine. A browser application can use SQLite's relational operations while the supplied JavaScript code handles interaction with the compiled module. This creates an official starting point for an environment where file access and program lifetimes differ from a native process.
I value SQLite's ability to put a database inside an application. A browser build extends that placement choice. Its usefulness depends on making the browser's execution and storage boundaries visible, because the presence of a SQL engine alone does not establish where a database persists or which thread executes a query.
Initialization produces the API object
The basic demonstration shipped with 3.40.0 starts by loading and initializing the module. It calls sqlite3InitModule and waits for the resulting promise before executing database code. The initialized object provides both a C-style API under capi and a higher-level object interface under oo1.
That sequence matters because loading a JavaScript file and having a ready database engine are distinct events. The initialization promise supplies the point at which application code receives the usable API. Code depending on the module should run after that point rather than assume that script inclusion alone completed the WebAssembly setup.
The demonstration redirects standard output and error output through functions supplied during initialization. This gives a browser host control over how messages appear. A native program can rely on process streams, while a page or worker needs to decide where those messages go. The initializer provides a place to establish that integration.
The example also retrieves the library version and source identifier through the C-style API. Those values identify the actual engine present in the module. They are useful when a browser application packages an engine independently of the operating system's SQLite installation or another database library on the user's machine.
JavaScript convenience does not remove ownership
The object interface opens a database through sqlite3.oo1.DB. The demo uses a transient database and surrounds its lifetime with try and finally, closing the database in the finalizer. Its comments explicitly warn against relying on garbage collection to release databases and prepared statements.
This requirement follows from wrapping resources whose lifetimes are not defined solely by ordinary JavaScript object reachability. An object wrapper makes the interface convenient, but the application still needs to end the database's lifetime explicitly. A JavaScript exception should not bypass that cleanup.
The following example uses the released demo's interface and parameter-binding style. It assumes that the module's JavaScript loader is already available. The database is transient, so this example demonstrates execution without claiming browser persistence.
sqlite3InitModule().then((sqlite3) => {
const db = new sqlite3.oo1.DB('/example.sqlite3', 'ct');
try {
db.exec('CREATE TABLE reading(value INTEGER)');
db.exec({
sql: 'INSERT INTO reading(value) VALUES (?)',
bind: [42]
});
const rows = [];
db.exec({
sql: 'SELECT value FROM reading',
rowMode: 'object',
resultRows: rows
});
console.log(rows);
} finally {
db.close();
}
});
The SQL remains recognizable, while the wrapper specifies how to bind inputs and collect output. JavaScript owns the resultRows array. The database has a separate lifetime ending at close. Keeping those lifetimes distinct makes the example's control flow easier to reason about.
Prepared statements remain explicit resources
The shipped demo also uses db.prepare to create a statement. It binds arguments, steps the statement, resets it, and reuses it for more values. A nested try and finally calls finalize after the loop. This mirrors the distinction between a database connection and a compiled statement executing within that connection.
The wrapper offers several conveniences without hiding the underlying sequence. stepReset combines steps that the demo also expresses separately. Binding can use a positional array or named values. The code still makes statement cleanup explicit, so a thrown exception cannot leave ownership dependent on eventual JavaScript collection.
The demo comments suggest that application code can often use DB.exec to avoid managing statement lifetimes directly. That is a useful interface choice. A caller needing simple execution can use a method that owns the temporary statement, while a caller deliberately reusing a prepared statement takes responsibility for its lifetime.
This difference matters to examples copied into applications. A short query inside a callback and a retained statement reused across interactions have different cleanup needs. The demonstration supplies both forms, allowing an application to choose based on its actual execution pattern.
Row representation is chosen by the caller
The object interface supports several row modes in the released demo. Array mode supplies values by position. Object mode supplies named properties. Statement mode passes an object from which the callback can retrieve values. The example also selects individual columns by number or name.
Those choices connect a relational result to JavaScript's data structures. A caller aggregating one numeric column does not necessarily need a full object for every row. Another caller rendering a record may prefer property names. The interface lets the application express the representation needed by the next operation.
The same execution method can collect rows in an array or invoke a callback for each row. Collecting makes a complete result available after execution, while callbacks allow work as rows are delivered. Neither form removes the cost of the query itself. They define how the caller receives the result that SQLite computes.
The demo also collects result-column names independently. This is useful for tools that inspect arbitrary queries, where the application cannot hard-code a schema-specific display. A browser SQL editor needs both the values and enough metadata to present them. Official examples of these paths make the browser build more usable as an application component.
Transactions interact with JavaScript exceptions
The demonstration supplies a callback to db.transaction, deletes rows inside it, and deliberately throws a SQLite3Error. After handling the exception, it queries the row count again to show rollback. It separately demonstrates nested savepoint callbacks and a thrown error inside the inner operation.
These examples connect JavaScript control flow to the database's transaction scope. A callback completing successfully and a callback throwing an exception have different outcomes. The wrapper must translate that distinction into the appropriate database operation, while allowing the caller to inspect the resulting error.
The scope is local to the demonstrated database operations. A transaction rollback does not reverse arbitrary JavaScript effects outside SQLite. If a callback also changes another system, the application needs a separate design for that effect. The example demonstrates how database work behaves when its callback fails, not a general transaction over every action the browser can perform.
The same demo registers a scalar function named twice, implemented by a JavaScript callback. It invokes that function through SQL and then deliberately calls it with the wrong argument count to demonstrate an error. This exposes another integration point between the host language and database evaluation. A SQL expression can call application logic through the supplied interface, while the database still checks the function's invocation contract. Running SQLite in WebAssembly therefore includes access to host-defined behavior, with errors that the JavaScript caller can catch and inspect.
Browser storage requires browser capabilities
The build README shipped in this release requires Emscripten for compilation. It also explains that the demonstration files must be served over HTTP. Opening the HTML directly through a file URL does not provide the loading environment the WebAssembly application needs.
For the OPFS-related demonstrations, the README identifies additional requirements involving SharedArrayBuffer and Atomics. The server must emit the necessary cross-origin response headers. This places part of deployment configuration outside the database initialization code. An application can load SQLite successfully while still lack the browser capabilities required by a particular storage integration.
The basic demo handles main-thread and worker execution differently. In a worker it sends log messages to the page, and it takes care to resolve the module location correctly. That reflects another browser-specific integration boundary. The database API can be similar across contexts while loading and communication differ.
I see the value of the official build in these concrete interfaces and examples. They show when the engine is ready, who owns statements, and which browser configuration the storage demonstrations require. SQLite can supply relational execution inside the browser while the application deliberately chooses how to load it, where to run it, and what persistence guarantees its selected storage path provides.