Fix: Tauri Not Working — Command Not Found, IPC Error, File System Permission Denied, or Build Fails
Quick Answer
How to fix Tauri app issues — Rust command registration, invoke IPC, tauri.conf.json permissions, fs scope, window management, and common build errors on Windows/macOS/Linux.
The Problem
invoke throws “command not found”:
import { invoke } from '@tauri-apps/api/core';
const result = await invoke('my_command', { name: 'Alice' });
// Error: Command not found: my_commandOr file system operations are rejected:
import { readTextFile } from '@tauri-apps/plugin-fs';
const content = await readTextFile('/home/user/config.json');
// Error: path not allowed on the configured scopeOr tauri dev runs but invoke always returns undefined:
#[tauri::command]
fn greet(name: String) -> String {
format!("Hello, {}!", name)
}
// JS receives undefined instead of the stringOr the build fails with a Rust compilation error:
error[E0425]: cannot find function `greet` in this scope
--> src-tauri/src/main.rs:12:5Why This Happens
Tauri has a strict separation between the Rust backend and the frontend:
- Commands must be registered in
generate_handler!— defining a#[tauri::command]function isn’t enough. You must list every command ingenerate_handler![my_command]and pass it to.invoke_handler(). - Tauri v1 vs v2 API differences — Tauri v2 restructured all APIs into plugins (
@tauri-apps/plugin-fs,@tauri-apps/plugin-shell, etc.). Using v1 import paths with a v2 project (or vice versa) fails at import time. - Permissions are locked down by default — in Tauri v2, all capabilities (file system, shell, HTTP) require explicit permission grants in
capabilities/*.json. No permission = rejected at runtime. - Return types must implement
serde::Serialize— Rust commands communicate with the frontend through JSON serialization. If your return type doesn’t implementSerialize, the command silently returnsundefinedor fails.
Fix 1: Register Commands Correctly
Every Rust command must be registered in main.rs:
// src-tauri/src/main.rs
// WRONG — command defined but not registered
#[tauri::command]
fn greet(name: String) -> String {
format!("Hello, {}!", name)
}
fn main() {
tauri::Builder::default()
.run(tauri::generate_context!())
.expect("error while running tauri application");
// Missing: .invoke_handler(tauri::generate_handler![greet])
}
// CORRECT — command defined AND registered
#[tauri::command]
fn greet(name: String) -> String {
format!("Hello, {}!", name)
}
#[tauri::command]
async fn fetch_data(url: String) -> Result<String, String> {
// Async commands must return Result<T, E> where E: Serialize
reqwest::get(&url)
.await
.map_err(|e| e.to_string())?
.text()
.await
.map_err(|e| e.to_string())
}
fn main() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![
greet,
fetch_data,
// List ALL commands here
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}Organize commands in separate modules:
// src-tauri/src/commands/user.rs
#[tauri::command]
pub fn get_user(id: u32) -> User {
// ...
}
// src-tauri/src/commands/file.rs
#[tauri::command]
pub async fn read_config() -> Result<Config, String> {
// ...
}
// src-tauri/src/commands/mod.rs
pub mod user;
pub mod file;
// src-tauri/src/main.rs
mod commands;
fn main() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![
commands::user::get_user,
commands::file::read_config,
])
.run(tauri::generate_context!())
.expect("error");
}Fix 2: Ensure Return Types Are Serializable
Commands must return types that implement serde::Serialize:
// WRONG — custom struct without Serialize
struct User {
id: u32,
name: String,
}
#[tauri::command]
fn get_user(id: u32) -> User { // Compile error: User doesn't implement Serialize
User { id, name: "Alice".to_string() }
}
// CORRECT — derive Serialize (and Deserialize for inputs)
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
pub struct User {
pub id: u32,
pub name: String,
pub email: Option<String>,
}
#[tauri::command]
fn get_user(id: u32) -> User {
User { id, name: "Alice".to_string(), email: None }
}
// Error handling — use Result for fallible operations
#[derive(Serialize)]
pub struct AppError {
message: String,
code: u32,
}
impl From<std::io::Error> for AppError {
fn from(e: std::io::Error) -> Self {
AppError { message: e.to_string(), code: 1 }
}
}
#[tauri::command]
async fn read_file(path: String) -> Result<String, AppError> {
let content = tokio::fs::read_to_string(&path).await?; // ? converts via From
Ok(content)
}Frontend error handling:
import { invoke } from '@tauri-apps/api/core';
try {
const user = await invoke<User>('get_user', { id: 1 });
console.log(user.name);
} catch (e) {
// e is the serialized AppError from Rust
console.error(e); // { message: "...", code: 1 }
}Fix 3: Configure Permissions (Tauri v2)
Tauri v2 requires explicit capability grants in JSON files:
// src-tauri/capabilities/default.json
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Default app capabilities",
"windows": ["main"],
"permissions": [
"core:default",
// File system — must specify exactly what's needed
"fs:allow-read-text-file",
"fs:allow-write-text-file",
"fs:allow-read-dir",
"fs:allow-create-dir",
"fs:allow-remove-file",
// Shell
"shell:allow-open",
// HTTP client
"http:default",
// Dialog
"dialog:allow-open",
"dialog:allow-save",
// Clipboard
"clipboard-manager:allow-read-text",
"clipboard-manager:allow-write-text",
// Window
"window:allow-maximize",
"window:allow-minimize",
"window:allow-close",
"window:allow-set-title"
]
}File system scope — restrict which paths are accessible:
// src-tauri/capabilities/default.json
{
"permissions": [
{
"identifier": "fs:allow-read-text-file",
"allow": [
{ "path": "$APPDATA/**" }, // App data directory
{ "path": "$HOME/Documents/**" },
{ "path": "$DESKTOP/**" }
],
"deny": [
{ "path": "$HOME/.ssh/**" } // Explicitly deny sensitive paths
]
},
{
"identifier": "fs:allow-write-text-file",
"allow": [
{ "path": "$APPDATA/**" } // Only write to app data
]
}
]
}Available path variables:
| Variable | Path |
|---|---|
$APPDATA | Platform app data directory |
$APPCONFIG | Platform app config directory |
$APPLOG | Platform app log directory |
$APPLOCALDATA | Platform local app data |
$HOME | User home directory |
$DESKTOP | User desktop |
$DOCUMENT | User documents |
$DOWNLOAD | User downloads |
$TEMP | System temp directory |
Fix 4: Access the App State from Commands
Pass state (database connections, config) to commands via Tauri’s managed state:
// src-tauri/src/main.rs
use std::sync::Mutex;
use tauri::State;
// Define your state struct
pub struct AppState {
pub db: Mutex<DatabaseConnection>,
pub config: AppConfig,
}
// Command that uses state
#[tauri::command]
async fn get_users(state: State<'_, AppState>) -> Result<Vec<User>, String> {
let db = state.db.lock().map_err(|e| e.to_string())?;
db.query_users().await.map_err(|e| e.to_string())
}
#[tauri::command]
fn get_config(state: State<'_, AppState>) -> AppConfig {
state.config.clone()
}
fn main() {
let db = DatabaseConnection::new("sqlite://app.db").expect("DB connect failed");
let config = AppConfig::load().expect("Config load failed");
tauri::Builder::default()
.manage(AppState {
db: Mutex::new(db),
config,
})
.invoke_handler(tauri::generate_handler![get_users, get_config])
.run(tauri::generate_context!())
.expect("error");
}Emit events from Rust to the frontend:
use tauri::{AppHandle, Manager, Emitter};
#[tauri::command]
async fn start_long_task(app: AppHandle) -> Result<(), String> {
for i in 0..=100 {
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
// Emit progress event to all windows
app.emit("task-progress", i)
.map_err(|e| e.to_string())?;
}
app.emit("task-complete", ()).map_err(|e| e.to_string())?;
Ok(())
}// Frontend — listen for events
import { listen } from '@tauri-apps/api/event';
const unlisten = await listen('task-progress', (event) => {
progressBar.value = event.payload;
});
await listen('task-complete', () => {
console.log('Task done!');
unlisten(); // Stop listening
});
await invoke('start_long_task');Fix 5: Fix Common Build Errors
Cargo.toml missing plugin dependencies:
# src-tauri/Cargo.toml
[dependencies]
tauri = { version = "2", features = [] }
tauri-plugin-fs = "2" # Add plugins you use
tauri-plugin-shell = "2"
tauri-plugin-http = "2"
tauri-plugin-dialog = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] } # For async commands
[build-dependencies]
tauri-build = { version = "2", features = [] }Register plugins in main.rs:
fn main() {
tauri::Builder::default()
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_http::init())
.plugin(tauri_plugin_dialog::init())
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}Frontend package.json:
{
"dependencies": {
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-fs": "^2",
"@tauri-apps/plugin-shell": "^2",
"@tauri-apps/plugin-http": "^2",
"@tauri-apps/plugin-dialog": "^2"
}
}Fix 6: Debug Tauri Apps
// Frontend — check if running inside Tauri
import { invoke } from '@tauri-apps/api/core';
const isTauri = typeof window !== 'undefined' && '__TAURI__' in window;
// Enable debug logging in Rust
// src-tauri/src/main.rs
fn main() {
// Set up logging
tauri::Builder::default()
.plugin(tauri_plugin_log::Builder::new()
.target(tauri_plugin_log::Target::new(
tauri_plugin_log::TargetKind::LogDir { file_name: Some("app.log".to_string()) }
))
.build())
// ...
}
// In Rust commands — use log macros
use log::{debug, info, error};
#[tauri::command]
fn process_data(data: String) -> Result<String, String> {
debug!("Processing data: {}", &data[..20.min(data.len())]);
let result = do_work(&data).map_err(|e| {
error!("Processing failed: {}", e);
e.to_string()
})?;
info!("Processing complete, result length: {}", result.len());
Ok(result)
}Run with verbose output:
# Show Rust log output
RUST_LOG=debug tauri dev
# Show Tauri-specific logs
RUST_LOG=tauri=debug tauri dev
# Check the generated code
cargo expand --manifest-path src-tauri/Cargo.toml # Requires cargo-expandStill Not Working?
invoke returns undefined for a void command — Rust commands that return () (unit type) correctly map to undefined in JavaScript. If you expect undefined, this is correct behavior. If you expected a value, add a return type to your Rust function and a corresponding Result<T, E>.
Window opens but stays blank (white screen) — check the devUrl in tauri.conf.json. In development, Tauri opens a URL (e.g., http://localhost:1420). If the dev server isn’t running, the window is blank. Run npm run dev in the frontend directory first, then tauri dev.
macOS code signing errors in production — Tauri apps require code signing for macOS distribution. Set up APPLE_CERTIFICATE, APPLE_CERTIFICATE_PASSWORD, APPLE_SIGNING_IDENTITY, APPLE_ID, and APPLE_PASSWORD environment variables in your CI. For local testing without a certificate, use tauri build --debug or disable Gatekeeper temporarily.
Permission errors on Windows with fs plugin — Windows paths use backslashes, but Tauri’s path scope uses forward slashes internally. Use path variables ($APPDATA/**) instead of hardcoded Windows paths in your capability file. Use path.join() or Tauri’s path API to construct platform-appropriate paths at runtime.
For related Rust issues, see Fix: Rust Error Handling Not Working and Fix: Rust Borrow Checker Error.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Electron Not Working — Window Not Showing, IPC Not Communicating, or Build Failing
How to fix Electron issues — main and renderer process setup, IPC communication with contextBridge, preload scripts, auto-update, native module rebuilding, and packaging with electron-builder.
Fix: Deno PermissionDenied — Missing --allow-read, --allow-net, and Other Flags
How to fix Deno PermissionDenied (NotCapable in Deno 2) errors — the right permission flags, path-scoped permissions, deno.json permission sets, and the Deno.permissions API.
Fix: Fastify Not Working — 404, Plugin Encapsulation, and Schema Validation Errors
How to fix Fastify issues — route 404 from plugin encapsulation, reply already sent, FST_ERR_VALIDATION, request.body undefined, @fastify/cors, hooks not running, and TypeScript type inference.
Fix: Kafka Consumer Not Receiving Messages, Connection Refused, and Rebalancing Errors
How to fix Apache Kafka issues — consumer not receiving messages, auto.offset.reset, Docker advertised.listeners, max.poll.interval.ms rebalancing, MessageSizeTooLargeException, and KafkaJS errors.