$ cat /posts/tauri-20-error-handling-best-practices.md
[tags]Tauri 2.0

Tauri 2.0 Error Handling Best Practices

drwxr-xr-x2026-01-295 min0 views
Tauri 2.0 Error Handling Best Practices

Error handling in Tauri 2.0 ensures applications respond gracefully to failures providing clear feedback and maintaining stability combining Rust Result types with JavaScript error catching enabling robust applications recovering from errors without crashes—critical practice for production applications maintaining user experience during failures, preserving application state, and enabling debugging through proper error reporting maintaining reliability users expect from desktop software. Error handling strategy combines Rust Result and Option types expressing fallible operations explicitly, custom error types providing contextual information, error propagation with ? operator simplifying error flow, frontend error boundaries catching JavaScript errors, user-friendly error messages avoiding technical jargon, error logging capturing details for debugging, and graceful degradation maintaining partial functionality when errors occur delivering comprehensive error management. This comprehensive guide covers understanding error architecture across Rust and JavaScript boundary, implementing Rust Result types with custom errors, using thiserror for error derivation, propagating errors properly with context, creating error boundaries in React catching component errors, displaying user-friendly error messages, implementing retry logic for transient failures, logging errors with context for debugging, and real-world examples including command error handling with validation, async operation error recovery, and complete error reporting system maintaining application stability through disciplined error handling practices. Mastering error patterns enables building professional desktop applications handling failures gracefully maintaining user trust through proper error management and recovery. Before proceeding, understand commands, events, and logging.

Rust Error Handling with Result Types

Rust uses Result and Option types for explicit error handling. Understanding Result types enables building reliable backend operations with proper error propagation maintaining type safety and compile-time error checking.

rustrust_error_handling.rs
// Basic Result usage
#[tauri::command]
fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        Err("Division by zero".to_string())
    } else {
        Ok(a / b)
    }
}

// Custom error types with thiserror
// Cargo.toml: thiserror = "1.0"
use thiserror::Error;

#[derive(Error, Debug)]
pub enum AppError {
    #[error("File not found: {0}")]
    FileNotFound(String),
    
    #[error("Invalid input: {0}")]
    InvalidInput(String),
    
    #[error("Database error: {0}")]
    DatabaseError(String),
    
    #[error("Network error: {0}")]
    NetworkError(String),
    
    #[error("Permission denied")]
    PermissionDenied,
    
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),
    
    #[error("Parse error: {0}")]
    Parse(#[from] std::num::ParseIntError),
}

// Using custom errors in commands
#[tauri::command]
fn read_config(path: String) -> Result<String, AppError> {
    if path.is_empty() {
        return Err(AppError::InvalidInput("Path cannot be empty".to_string()));
    }

    let content = std::fs::read_to_string(&path)
        .map_err(|_| AppError::FileNotFound(path.clone()))?;

    Ok(content)
}

// Error propagation with ?
#[tauri::command]
async fn fetch_and_save(
    url: String,
    output_path: String,
) -> Result<String, AppError> {
    // Fetch data (? propagates errors)
    let response = reqwest::get(&url)
        .await
        .map_err(|e| AppError::NetworkError(e.to_string()))?;

    let data = response.text()
        .await
        .map_err(|e| AppError::NetworkError(e.to_string()))?;

    // Save to file (? automatically converts io::Error to AppError)
    std::fs::write(&output_path, &data)?;

    Ok(format!("Saved {} bytes to {}", data.len(), output_path))
}

// Handling multiple error types with anyhow
// Cargo.toml: anyhow = "1.0"
use anyhow::{Result, Context, bail};

#[tauri::command]
async fn complex_operation(id: i32) -> Result<String> {
    if id < 0 {
        bail!("Invalid ID: must be positive");
    }

    let config = load_config()
        .context("Failed to load configuration")?;

    let data = fetch_data(id)
        .await
        .context(format!("Failed to fetch data for ID {}", id))?;

    process_data(&data)
        .context("Data processing failed")?;

    Ok("Success".to_string())
}

fn load_config() -> Result<Config> {
    // Config loading logic
    Ok(Config::default())
}

// Error context with custom types
#[derive(Debug)]
struct ErrorContext {
    operation: String,
    timestamp: chrono::DateTime<chrono::Utc>,
    user_id: Option<i32>,
}

impl ErrorContext {
    fn new(operation: impl Into<String>) -> Self {
        Self {
            operation: operation.into(),
            timestamp: chrono::Utc::now(),
            user_id: None,
        }
    }

    fn with_user(mut self, user_id: i32) -> Self {
        self.user_id = Some(user_id);
        self
    }
}

#[derive(Error, Debug)]
#[error("{context:?}: {source}")]
pub struct ContextualError {
    context: ErrorContext,
    #[source]
    source: Box<dyn std::error::Error + Send + Sync>,
}

// Validation with Result
fn validate_email(email: &str) -> Result<(), AppError> {
    if !email.contains('@') {
        return Err(AppError::InvalidInput(
            "Email must contain @ symbol".to_string()
        ));
    }
    if email.len() < 5 {
        return Err(AppError::InvalidInput(
            "Email too short".to_string()
        ));
    }
    Ok(())
}

#[tauri::command]
fn register_user(
    email: String,
    password: String,
) -> Result<String, AppError> {
    validate_email(&email)?;
    
    if password.len() < 8 {
        return Err(AppError::InvalidInput(
            "Password must be at least 8 characters".to_string()
        ));
    }

    // Registration logic
    Ok("User registered successfully".to_string())
}

// Serializable errors for frontend
use serde::Serialize;

#[derive(Serialize)]
struct CommandError {
    code: String,
    message: String,
    details: Option<String>,
}

impl From<AppError> for CommandError {
    fn from(error: AppError) -> Self {
        let (code, message) = match error {
            AppError::FileNotFound(path) => (
                "FILE_NOT_FOUND".to_string(),
                format!("File not found: {}", path),
            ),
            AppError::InvalidInput(msg) => (
                "INVALID_INPUT".to_string(),
                msg,
            ),
            AppError::PermissionDenied => (
                "PERMISSION_DENIED".to_string(),
                "You don't have permission".to_string(),
            ),
            _ => (
                "INTERNAL_ERROR".to_string(),
                error.to_string(),
            ),
        };

        CommandError {
            code,
            message,
            details: None,
        }
    }
}

// Handling Option with ok_or
fn get_user(id: i32) -> Result<User, AppError> {
    let users = get_all_users();
    
    users.iter()
        .find(|u| u.id == id)
        .cloned()
        .ok_or_else(|| AppError::InvalidInput(
            format!("User {} not found", id)
        ))
}

Frontend Error Handling

Frontend error handling catches JavaScript errors and Tauri command failures. Understanding error boundaries and try-catch enables building resilient UI maintaining user experience during errors with proper error display and recovery.

typescriptfrontend_error_handling.ts
// React Error Boundary
import React, { Component, ErrorInfo, ReactNode } from 'react';

interface Props {
  children: ReactNode;
  fallback?: ReactNode;
}

interface State {
  hasError: boolean;
  error: Error | null;
}

class ErrorBoundary extends Component<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    console.error('Error caught by boundary:', error, errorInfo);
    
    // Log to backend
    invoke('log_frontend_error', {
      error: error.message,
      stack: error.stack,
      componentStack: errorInfo.componentStack,
    });
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback || (
        <div className="error-container">
          <h1>Something went wrong</h1>
          <p>{this.state.error?.message}</p>
          <button onClick={() => this.setState({ hasError: false, error: null })}>
            Try again
          </button>
        </div>
      );
    }

    return this.props.children;
  }
}

// Usage
function App() {
  return (
    <ErrorBoundary>
      <MyComponent />
    </ErrorBoundary>
  );
}

// Handling Tauri command errors
import { invoke } from '@tauri-apps/api/core';

interface TauriError {
  code: string;
  message: string;
  details?: string;
}

async function executeCommand<T>(
  command: string,
  args?: any
): Promise<T> {
  try {
    const result = await invoke<T>(command, args);
    return result;
  } catch (error) {
    // Parse Tauri error
    if (typeof error === 'string') {
      throw new Error(error);
    }
    throw error;
  }
}

// Custom hook for error handling
import { useState } from 'react';

function useAsyncCommand<T>(command: string) {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [data, setData] = useState<T | null>(null);

  const execute = async (args?: any) => {
    setLoading(true);
    setError(null);
    
    try {
      const result = await invoke<T>(command, args);
      setData(result);
      return result;
    } catch (err) {
      const errorMessage = err instanceof Error ? err.message : String(err);
      setError(errorMessage);
      throw err;
    } finally {
      setLoading(false);
    }
  };

  const reset = () => {
    setError(null);
    setData(null);
  };

  return { execute, loading, error, data, reset };
}

// Usage
function FileLoader() {
  const { execute, loading, error, data } = useAsyncCommand<string>('read_file');

  const loadFile = async (path: string) => {
    try {
      await execute({ path });
    } catch (err) {
      // Error already set in state
      console.error('Failed to load file');
    }
  };

  return (
    <div>
      <button onClick={() => loadFile('/path/to/file')} disabled={loading}>
        {loading ? 'Loading...' : 'Load File'}
      </button>
      
      {error && (
        <div className="error-message">
          <strong>Error:</strong> {error}
        </div>
      )}
      
      {data && <pre>{data}</pre>}
    </div>
  );
}

// Retry logic for transient errors
async function withRetry<T>(
  operation: () => Promise<T>,
  maxRetries: number = 3,
  delay: number = 1000
): Promise<T> {
  let lastError: Error;

  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await operation();
    } catch (error) {
      lastError = error as Error;
      
      if (attempt < maxRetries - 1) {
        await new Promise(resolve => setTimeout(resolve, delay * (attempt + 1)));
      }
    }
  }

  throw lastError!;
}

// Usage
const fetchData = async () => {
  return withRetry(
    () => invoke<Data>('fetch_data'),
    3,  // Max 3 retries
    1000 // 1 second initial delay
  );
};

// Error toast notifications
import { toast } from 'react-toastify';

function handleCommandError(error: unknown) {
  const message = error instanceof Error ? error.message : String(error);
  
  if (message.includes('FILE_NOT_FOUND')) {
    toast.error('File not found. Please select a valid file.');
  } else if (message.includes('PERMISSION_DENIED')) {
    toast.error('Permission denied. Please check file permissions.');
  } else if (message.includes('NETWORK_ERROR')) {
    toast.error('Network error. Please check your connection.');
  } else {
    toast.error(`An error occurred: ${message}`);
  }
}

// Global error handler
window.addEventListener('unhandledrejection', (event) => {
  console.error('Unhandled promise rejection:', event.reason);
  
  invoke('log_unhandled_error', {
    error: String(event.reason),
    type: 'unhandled_rejection',
  });
});

window.addEventListener('error', (event) => {
  console.error('Uncaught error:', event.error);
  
  invoke('log_unhandled_error', {
    error: event.error?.message || String(event.error),
    stack: event.error?.stack,
    type: 'uncaught_error',
  });
});

User-Friendly Error Messages

Technical ErrorUser-Friendly MessageAction
FileNotFoundThe file could not be foundCheck file path
PermissionDeniedAccess deniedCheck permissions
NetworkErrorConnection failedCheck internet
ParseErrorInvalid file formatUse correct format
DatabaseErrorData save failedTry again later

Error Handling Best Practices

  • Use Result Types: Express fallible operations with Result
  • Custom Error Types: Create domain-specific error enums
  • Add Context: Include relevant information with errors
  • Propagate Properly: Use ? operator for cleaner error flow
  • User-Friendly Messages: Translate technical errors to clear messages
  • Log Errors: Capture error details for debugging
  • Handle Boundaries: Use error boundaries in React catching failures
  • Retry Transient: Implement retry logic for network errors
  • Fail Gracefully: Maintain partial functionality when possible
  • Test Error Paths: Verify error handling in tests
Pro Tip: Never show raw error messages to users! Technical errors like "FileNotFound: /var/data/app.db" confuse users. Instead, show friendly messages like "Could not access app data. Please restart the application." Log technical details for debugging while presenting clear, actionable messages to users!

Next Steps

Conclusion

Mastering error handling in Tauri 2.0 enables building professional desktop applications responding gracefully to failures maintaining user experience and application stability through proper error management preventing crashes and maintaining partial functionality when errors occur delivering reliable software users trust. Error handling strategy combines Rust Result and Option types expressing fallible operations explicitly with compile-time safety, custom error types providing contextual information for debugging, error propagation with ? operator simplifying error flow, frontend error boundaries catching JavaScript failures, user-friendly error messages avoiding technical jargon, comprehensive error logging capturing details for debugging, and graceful degradation maintaining functionality delivering robust error management. Understanding error patterns including Rust Result usage with custom error types and thiserror, error propagation with context and proper conversion, frontend error handling with boundaries and hooks, retry logic for transient failures, user-friendly message translation, and best practices maintaining reliability establishes foundation for building professional desktop applications handling failures gracefully maintaining user trust through proper error management and recovery maintaining application stability users depend on!

$ cat /comments/ (0)

new_comment.sh

// Email hidden from public

>_

$ cat /comments/

// No comments found. Be the first!

[session] guest@{codershandbook}[timestamp] 2026

Navigation

Categories

Connect

Subscribe

// 2026 {Coders Handbook}. EOF.