Inside the bridge: how JavaScript reaches a typed Rust command
Tauri's IPC bridge is the subject here: the mechanism that carries an invoke from JavaScript in a webview to a typed Rust command and brings the result back. The useful question is not whether the call looks like a function call, but what state, transport, validation, and scheduling make it work. Tracing that path lets you choose between ordinary JSON, raw bytes, asynchronous results, and ordered channels—and lets you distinguish a delivery failure from an authorization or application failure.
The JavaScript side: a promise is a small state machine
The public JavaScript API is intentionally simple. Code can call invoke("load_report", { id }) and await a promise. Tauri's internal implementation has to do much more before that promise can settle.
When invoke is called, the JavaScript core creates two callback entries: one for success and one for error. Each receives a generated numeric identifier. The success callback resolves the promise and removes the error callback; the error callback rejects the promise and removes the success callback. Both are registered as one-shot callbacks, so a normal request has a defined cleanup path rather than leaving both functions in memory after completion.
This callback map is also what makes an asynchronous Rust command possible. Rust does not hold a JavaScript function object and call it across the language boundary. It receives callback identifiers, then later asks the webview to run the corresponding callback. If the page reloads while Rust is still working, the JavaScript callback may no longer exist; the core warns that this can happen when an asynchronous operation outlives the page. A promise therefore has a lifecycle tied to both the Rust operation and the current webview document.
The internal core also queues calls until the IPC transport is installed. Initialization scripts are injected when the webview is prepared, but page execution and bridge setup still have an ordering problem: frontend code can become ready before the internal ipc function has been attached. Rather than failing immediately, the core stores the action and periodically waits for the transport. Once the function exists, the queued actions are sent.
The payload must then be represented in a form the chosen transport can carry. Tauri's serializer treats a binary-like top-level value as an octet-stream request. Other values become JSON, with special handling for maps, typed arrays, array buffers, and objects that expose Tauri's custom serialization key. This is why a Rust enum or a channel object can have a deliberate wire representation instead of relying on whatever a browser's default serializer happens to produce.
The serializer is a boundary, not a type system shared by both languages. Rust's command signature may use an enum, a struct, a borrowed string, or a framework argument such as managed state. JavaScript has to send a representation that the generated Rust wrapper can deserialize. If the representation is wrong, the failure occurs at argument extraction, before the application function performs its work.
Once the payload is ready, Tauri has two transport routes. The preferred route on platforms where it is available uses a custom ipc protocol through fetch. The JavaScript code converts the command into a URL associated with that protocol and sends a POST request. It attaches the content type, success and error callback identifiers, and a runtime-generated invoke key as headers.
The custom route gives the webview something that behaves like a local HTTP exchange. The Rust protocol handler can return a response body and a response header indicating success or error. JavaScript then chooses JSON, text, or an array buffer based on the response content type and passes the decoded value to the callback map. An OPTIONS request is also handled for preflight behavior; other methods are rejected.
This route is preferred, not guaranteed.