Plugin Errors
Diagnose and fix common Tauri v2 plugin issues including missing installations, initialization failures, version mismatches, permission gaps, and plugin-specific pitfalls
Plugins extend Tauri with native capabilities like file system access, dialogs, or a localhost server. When a plugin doesn't work, the root cause usually falls into one of a few predictable categories: the plugin was never installed, it was never activated, its version is incompatible, or it lacks the permissions it needs. This document walks through how to identify and fix each category, with concrete examples for a React + Vite frontend. The Plugins chapter covers installation and configuration in depth.
Plugin Not Installed
A plugin is two pieces: a Rust crate that lives in src-tauri/Cargo.toml and an npm package that sits in your frontend package.json. If either is missing, the plugin's commands won't be available at runtime, and depending on which side is absent, you'll get different error signatures.
Rust Side — Missing Crate Dependency
The Rust crate provides the plugin's backend logic. Without it, Tauri's build system won't know the plugin exists, and any attempt to call its commands from JavaScript will result in a "command not found" error (often a 400 Bad Request in the browser console for IPC calls).
Install the crate using Tauri's CLI shortcut when available:
pnpm tauri add fs
For plugins that don't have an automated installer, add the dependency manually in src-tauri/Cargo.toml:
[dependencies]
tauri-plugin-fs = "2"
After adding the crate, you must run at least cargo check or restart tauri dev for the changes to take effect. The plugin's commands won't appear until the Rust code is compiled.
Silent Failure From Missing Crate:
If you add the npm package but forget the Rust crate, invoke calls will reject with a 400 status and an error like unhandled promise rejection: command not found. The JavaScript side silently fails because Tauri's IPC layer cannot route commands that don't exist in the backend. Always verify that both sides of the plugin are installed.
Frontend Side — Missing npm Package
The npm package gives your React components access to typed JavaScript functions that call into the Rust plugin. If you try to import from a package that isn't in node_modules, you'll get a build error during npm run dev or npm run build.
Install the matching npm package:
pnpm add @tauri-apps/plugin-fs
Package Names Follow a Convention:
Official Tauri plugins use the pattern tauri-plugin-{name} for the Rust crate and @tauri-apps/plugin-{name} for the npm package. Community plugins may differ. Always check the plugin's documentation for exact package names.
Plugin Not Initialized
Having both packages installed is necessary but not sufficient. The plugin must be explicitly registered with the Tauri runtime on the Rust side, and sometimes also initialized on the JavaScript side.
Forgetting to Call .plugin() in Rust
Every plugin crate exposes an initializer function that must be attached to the tauri::Builder. Skipping this step means the plugin's commands are compiled into the binary but never registered — they sit dormant. Calls from the frontend will again produce a "command not found" error.
The initialization happens in the run function, typically inside src-tauri/src/lib.rs:
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_fs::init()) // Without this line, fs commands are unavailable
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
If your app uses multiple plugins, each needs its own .plugin() call:
tauri::Builder::default()
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_shell::init())
.run(tauri::generate_context!())
.expect("error while running tauri application");
Forgetting to Import and Initialize in Frontend
Some plugins require no frontend initialization — their commands are available directly after calling .plugin() in Rust. Others need a setup step in JavaScript before their APIs work. The localhost plugin, for example, must be imported and initialized on both sides.
A missing frontend initialization can cause the plugin to appear functional but silently fail to intercept requests. Always consult the plugin's documentation for any required JavaScript setup.
Version Mismatch
Plugins evolve alongside Tauri itself. A mismatch between the Tauri core version and a plugin's expected version, or between the Rust and JavaScript packages of the same plugin, produces errors that range from compilation failures to subtle runtime misbehaviors.
Mixing Tauri v1 and v2 Plugins
Tauri v2 introduced a new plugin architecture with a different trait system. Plugins written for v1 (using tauri::plugin::Plugin from the v1 API) cannot be used with a v2 app. If you attempt to register a v1 plugin in a v2 Builder, you'll encounter a compilation error similar to:
the trait bound `TauriPlugin<_>: Plugin<Wry>` is not satisfied
This happens because the v1 and v2 Plugin traits are different types to the Rust compiler, even if they look similar. The fix is to use the v2 version of every plugin. The main plugin repository has a v2 branch, and crates.io lists v2-compatible versions starting at 2.0.0.
To migrate, update your Cargo.toml dependencies:
# Before (v1)
tauri-plugin-store = "1"
# After (v2)
tauri-plugin-store = "2"
Old Tutorials May Reference v1 Patterns:
If you follow a tutorial and get trait errors, check the plugin version it's using. Many v1 examples call PluginBuilder::default().build() or use tauri_plugin_store::PluginBuilder. In v2, the pattern is tauri_plugin_store::init() or tauri_plugin_store::Builder::new().build(). Version 2 plugins expose a simpler, unified initialization API.
Incompatible Rust and JavaScript Plugin Versions
The npm package and the Rust crate for a plugin share an IPC protocol. If they drift apart in version — for instance, you update the npm package to 2.1.0 but keep the crate at 2.0.0 — the command signatures may not align. This can cause parameter serialization errors, missing commands, or 400 responses with opaque messages.
A safe practice is to keep both at the same semver minor range. If you use pnpm tauri add fs, it updates both sides automatically. For manual updates, check the plugin's changelog for any breaking changes between versions.
Permission and Capability Errors
Even if a plugin is installed, initialized, and version-matched, its commands can be blocked by Tauri's security model. Every plugin command requires an explicit permission in a capability file. Missing permissions are one of the most common causes of plugin errors in production.
Missing Plugin Permission in Capabilities
Each plugin defines autogenerated permissions for its commands. For example, the fs plugin exposes fs:allow-write-text-file to enable writing text files. If you call writeTextFile from JavaScript without granting that permission, Tauri rejects the IPC call with an error like:
fs.write_text_file not allowed. Permissions associated with this command: fs:allow-app-write...
This error message is your diagnostic tool. It lists the permissions that are accepted for the command. To fix it, add the relevant permission to a capability file, typically under src-tauri/capabilities/default.json:
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"fs:default",
"fs:allow-write-text-file"
]
}
After adding the permission, restart tauri dev. The command should now succeed.
Quick Test for Permission Issues:
If a plugin command fails and you suspect a permission gap, try temporarily adding the plugin's default permission set (e.g., "fs:default"). If the command works after that, you've confirmed the issue is permission-related. Then narrow down to the specific fine-grained permission you need instead of leaving the broad default in place.
Scope Configuration Errors
Some permissions require a scope — a path, a URL, or a resource that the command is allowed to access. The fs plugin, for instance, needs scope entries that specify which files the application can read or write. A missing or incorrect scope produces a permission error that mentions the scope restriction.
When you need to write to a specific file, such as $HOME/test.txt, the capability must include a scope entry:
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"fs:default",
{
"identifier": "fs:allow-write-text-file",
"allow": [{ "path": "$HOME/test.txt" }]
}
]
}
Scopes can use Tauri's path variables like $HOME, $APPDATA, or $RESOURCE. If you reference a file without a matching scope, Tauri denies the access even if the command permission itself is present.
Common Plugin-Specific Errors
Beyond the generic categories above, certain plugins have recurring pitfalls that trap newcomers. Knowing these ahead of time saves debugging hours.
localhost Plugin — Window Label Conflict
The localhost plugin lets you serve a frontend from a local development server during production builds. A common setup error involves creating a second window with the same label as the one defined in tauri.conf.json.
By default, Tauri creates a window labeled "main" from the configuration file. If your Rust code tries to build another window with the same label using WindowBuilder::new(app, "main", ...), you'll get:
error encountered during setup hook: a webview with label 'main' already exists
The fix is to either remove the window definition from tauri.conf.json (so only the programmatically created window exists) or use a different label for the new window. The recommended approach for the localhost plugin is to remove the configuration-based window and create it entirely from the setup hook:
use tauri::Manager;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
let port = 5173;
tauri::Builder::default()
.plugin(tauri_plugin_localhost::Builder::new(port).build())
.setup(move |app| {
let url = format!("http://localhost:{}", port).parse().unwrap();
tauri::WebviewWindowBuilder::new(app, "main", tauri::WebviewUrl::External(url))
.title("My App")
.build()?;
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
Additionally, the localhost plugin requires remote API access. You must configure a remote domain scope in the capability file:
{
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"remote": {
"urls": ["http://localhost:5173"]
},
"permissions": [
"core:default"
]
}
Without the remote configuration, IPC commands from the externally loaded page will be blocked.
Dialog and Window Unresponsiveness — IPC Argument Name Conflict
A particularly tricky bug causes the entire app to freeze after calling certain plugin APIs like dialog.open() or window.currentMonitor(). The app becomes unresponsive, and no error appears in the console.
The root cause is a collision between the JavaScript invoke payload and an internal options field used by Tauri's IPC layer. If a command's parameter is named options, the IPC dispatcher conflates the command argument with the internal transport options, causing the promise to never resolve.
This is a Rust-side issue. If you define a custom command with a parameter named options, it will trigger the same freeze. The fix is to rename the parameter:
// ❌ Causes the app to hang
#[tauri::command]
fn my_command(options: &str) -> String {
format!("Received: {}", options)
}
// ✅ Works correctly
#[tauri::command]
fn my_command(opts: &str) -> String {
format!("Received: {}", opts)
}
This bug primarily affects custom commands, but if you're using a plugin that internally registers commands with an options parameter, you may encounter it during alpha or beta releases. In stable Tauri v2, core plugins avoid this naming pattern, but it's worth remembering if you ever wrap plugin functionality with your own commands.
Frozen App With No Error:
If your app hangs completely after an API call and the browser devtools also freeze, suspect a stuck IPC promise. Check any custom Rust commands for a parameter named options. Renaming it and restarting the app resolves the freeze.
Store Plugin — Trait Bound Errors During Compilation
The store plugin (and a few others) had significant API changes between v1 and v2. In v1, you'd use PluginBuilder::default().build(). In v2, the initialization pattern is tauri_plugin_store::init() or tauri_plugin_store::Builder::new().build().
If you copy v1 code into a v2 project, you'll get a compilation error about unsatisfied trait bounds — the same mismatch described earlier under version mixing. The solution is to use the v2 crate and the v2 initialization method:
// v2 correct initialization
tauri::Builder::default()
.plugin(tauri_plugin_store::Builder::new().build())
.run(tauri::generate_context!())
.expect("error");
Debugging Plugin Issues
When a plugin doesn't behave as expected, a structured debugging approach helps identify the cause faster than trial and error.
Using Rust Console and Backtraces
The terminal where you run tauri dev prints Rust-level errors. If a plugin fails to initialize, you'll often see a panic or a Result error printed here. You can increase verbosity by setting the RUST_BACKTRACE environment variable:
# Linux / macOS
RUST_BACKTRACE=1 pnpm tauri dev
# Windows PowerShell
$env:RUST_BACKTRACE=1
pnpm tauri dev
This produces a full stack trace pointing to the exact line where the error occurred, which is especially helpful for panics during plugin setup.
WebView DevTools
The browser-like developer tools inside the WebView are where JavaScript-side plugin errors appear. Right-click in the app window and select "Inspect Element," or use the shortcut Ctrl+Shift+I (Windows/Linux) or Cmd+Option+I (macOS).
Failed IPC calls show up as 400 Bad Request responses in the Network tab, and the Console tab will display the rejection message from invoke. The error message often includes the missing permission identifier or the command that wasn't found.
You can also open devtools programmatically for debug builds:
#[cfg(debug_assertions)]
{
let window = app.get_webview_window("main").unwrap();
window.open_devtools();
}
Verifying Capability Configuration
If a plugin command returns a permission error, the quickest way to verify the capability file is to check the generated schema. Tauri creates src-tauri/gen/schemas/desktop-schema.json automatically. You can manually inspect your capability files against this schema to ensure the permission identifiers exist and are spelled correctly.
A common mistake is referencing a permission like "fs:write-text-file" when the correct autogenerated permission is "fs:allow-write-text-file". The schema file lists all valid identifiers, so diff your capability file against it if you're unsure.
Summary
Plugin errors, while frustrating, are almost always mechanical — a missing line in a config file, a forgotten .plugin() call, or a permission string that doesn't match. The pattern is: install both sides, initialize in Rust, match versions, grant permissions in capabilities, and respect each plugin's unique setup requirements. Once a plugin is correctly wired, the real value of Tauri's native APIs opens up.