Skip to main content
On this page

Bindings

Coming in Deno 2.9

deno desktop ships in Deno v2.9.0 and is not in a stable release yet. To try it now, run deno upgrade canary to install the canary build. The command, configuration keys, and TypeScript APIs may still change before the feature is stable.

win.bind(name, handler) exposes a Deno-side function to the webview. From the webview, call it as bindings.<name>(args), and the call returns a Promise that resolves with the handler's return value.

Deno side
win.bind("readSettings", async () => {
  const text = await Deno.readTextFile("settings.json");
  return JSON.parse(text);
});

win.bind("saveSettings", async (settings) => {
  await Deno.writeTextFile("settings.json", JSON.stringify(settings, null, 2));
});
Webview side
const settings = await bindings.readSettings();
settings.theme = "dark";
await bindings.saveSettings(settings);

How it works Jump to heading

Bindings are not IPC. The Deno runtime and the rendering backend run as threads / processes inside the same address space (CEF) or coordinated process group (WebView). Calls go through in-process channels, and the backend dispatches them from its run loop.

This avoids the cross-process round-trip that socket-based IPC frameworks (Electron's ipcMain / ipcRenderer, Tauri's invoke) impose. Arguments and results are still encoded as they cross the realm boundary, but the transport is in-process: no socket, no cross-process scheduling.

In practical terms: bindings are fast enough that you do not need to worry about call frequency for typical app workloads.

The webview proxy Jump to heading

bindings on the webview side is a Proxy. Any property access creates a function on demand:

bindings.foo; // function
bindings.foo("a", 1); // Promise<unknown>

The proxy does not validate names: typing bindings.readSetings instead of bindings.readSettings does not throw at the property access; it throws when you call it (the call rejects because no such binding is registered).

Argument and return value semantics Jump to heading

Arguments and return values are encoded as JSON as they cross between the webview and the Deno runtime. This means:

  • Plain objects, arrays, strings, numbers, booleans, and null: passed through as-is.
  • Uint8Array: supported, for passing binary data.
  • undefined and optional properties: dropped during serialization.
  • Date, Map, Set, RegExp, typed arrays other than Uint8Array, ArrayBuffer: not preserved. Convert them to a JSON-compatible shape (a Date becomes a string, a Map becomes {}) before sending.
  • Functions, DOM nodes, prototypes, and cyclic references: not transferable.
  • Errors thrown by a handler: delivered to the webview as { name, message, stack } (see Errors below), not as an Error instance.

Stick to plain data and Uint8Array on both sides.

Async handlers Jump to heading

Handlers can be sync or async. The webview always sees a Promise:

win.bind("now", () => Date.now()); // sync
win.bind("delay", async (ms) => { // async
  await new Promise((r) => setTimeout(r, ms));
});
const t = await bindings.now();
await bindings.delay(500);

Errors Jump to heading

A handler that throws, synchronously or via a rejected promise, causes the webview-side call to reject:

win.bind("readFile", async (path) => {
  return await Deno.readTextFile(path);
});
try {
  await bindings.readFile("/missing");
} catch (e) {
  console.error(e); // NotFound: …
}

The error reaches the webview as a plain { name, message, stack } object. To distinguish error types, check error.name.

Unbinding Jump to heading

win.unbind("readSettings");

Removes the binding. Subsequent bindings.readSettings() calls reject.

Permissions Jump to heading

Bindings run inside the Deno runtime, so they inherit the process's permissions. A binding that calls Deno.readTextFile requires --allow-read to have been granted at startup. The webview cannot escalate the runtime's permissions through bindings.

For desktop apps you typically run with broad permissions baked into the compiled binary (deno desktop does not currently enforce a separate permission prompt at runtime). If you expose bindings that act on the filesystem or network, validate inputs as carefully as you would in any trust-boundary code.

Per-window bindings Jump to heading

Bindings are per-window. A binding registered on winA is not callable from winB's webview. To share, register on each window:

function bindShared(win: Deno.BrowserWindow) {
  win.bind("now", () => Date.now());
  win.bind("readSettings", readSettings);
}

const main = new Deno.BrowserWindow(); // adopts the startup window
bindShared(main);

const settings = new Deno.BrowserWindow();
bindShared(settings);

Type safety Jump to heading

There is no built-in type bridge between the Deno side's win.bind() and the webview side's bindings.<name>(). The two sides are separate JS realms.

A small shared declaration file gives you both ends:

bindings.d.ts
export interface Bindings {
  readSettings(): Promise<Settings>;
  saveSettings(s: Settings): Promise<void>;
  now(): Promise<number>;
}

declare global {
  // Make `bindings` typed in the webview.
  const bindings: Bindings;
}

export interface Settings {
  theme: "light" | "dark";
}

Reference it from the webview's tsconfig / Deno project config and use the same Bindings interface to type-check your win.bind calls. Mismatches between the registration and the declaration will be caught at compile time on the Deno side.

Migrating from Electron Jump to heading

If you are coming from Electron's ipcMain.handle('channel', handler) / ipcRenderer.invoke('channel', ...), the mental model is identical:

Electron deno desktop
ipcMain.handle('channel', (e, ...args) => result) win.bind('channel', (...args) => result)
ipcRenderer.invoke('channel', ...args) bindings.channel(...args)
contextBridge.exposeInMainWorld('api', {...}) Not needed; bindings is exposed by default.

The event object Electron passes as the first arg has no equivalent because there is no separate process to attribute the call to. Per-window context lives on the win you registered the binding on.

Last updated on

Did you find what you needed?

Edit this page
Privacy policy