| name | error-handler |
| description | Tauri 异常处理技能,覆盖 Rust 错误处理和 React 错误边界。
触发场景:
- 需要设计错误处理策略
- 需要处理 Rust Command 中的错误
- 需要处理前端 invoke 调用失败
- 需要实现全局错误处理
触发词:异常、错误处理、Error、Result、try-catch、panic、崩溃、错误边界
|
Tauri 异常处理
分层错误处理策略
前端 (React) 后端 (Rust)
┌──────────────────────┐ ┌──────────────────────┐
│ message.error() │ │ AppError 枚举 │
│ ErrorBoundary │ │ thiserror │
│ try-catch │ ◄─IPC─► │ Result<T, AppError> │
│ Ant Design Result │ │ 三层错误传播 │
└──────────────────────┘ └──────────────────────┘
Rust 错误处理
1. AppError 枚举(src-tauri/src/error.rs)
use thiserror::Error;
#[derive(Debug, Error)]
pub enum AppError {
#[error("IO 错误: {0}")]
Io(#[from] std::io::Error),
#[error("数据库错误: {0}")]
Database(#[from] rusqlite::Error),
#[error("JSON 解析错误: {0}")]
Json(#[from] serde_json::Error),
#[error("未找到: {0}")]
NotFound(String),
#[error("参数无效: {0}")]
InvalidInput(String),
#[error("{0}")]
Custom(String),
}
impl From<AppError> for String {
fn from(err: AppError) -> String {
err.to_string()
}
}
2. 三层错误传播
Database 层(返回 AppError)
impl Database {
pub fn get_config(&self, key: &str) -> Result<Option<String>, AppError> {
let conn = self.conn.lock()
.map_err(|e| AppError::Custom(e.to_string()))?;
let mut stmt = conn.prepare("SELECT value FROM app_config WHERE key = ?1")?;
let result = stmt
.query_row([key], |row| row.get::<_, String>(0))
.ok();
Ok(result)
}
}
Service 层(转换业务错误)
impl ConfigService {
pub fn get_required(&self, db: &Database, key: &str) -> Result<String, AppError> {
db.get_config(key)?
.ok_or_else(|| AppError::NotFound(format!("配置 {} 不存在", key)))
}
}
Command 层(转换为 String 给前端)
use tauri::State;
#[tauri::command]
pub fn get_config(db: State<'_, Database>, key: String) -> Result<String, String> {
db.get_config(&key)
.map_err(|e| e.to_string())?
.ok_or_else(|| format!("配置 {} 不存在", key))
}
3. Mutex 安全处理
let conn = self.conn.lock()
.map_err(|e| AppError::Custom(format!("锁定失败: {}", e)))?;
let conn = self.conn.lock().unwrap();
4. 错误传播模式
#[tauri::command]
fn read_config(path: String) -> Result<String, AppError> {
let content = std::fs::read_to_string(&path)?;
if content.is_empty() {
return Err(AppError::InvalidInput("配置文件为空".into()));
}
Ok(content)
}
#[tauri::command]
fn bad_read(path: String) -> String {
std::fs::read_to_string(&path).unwrap()
}
React 错误处理
1. invoke 错误处理(Ant Design)
import { message } from "antd";
import { invoke } from "@tauri-apps/api/core";
async function loadData() {
try {
const result = await invoke<DataType>("get_data");
setData(result);
message.success("加载成功");
} catch (error) {
message.error(String(error));
console.error("加载失败:", error);
}
}
2. 封装 API 调用(src/lib/api/index.ts)
import { invoke } from "@tauri-apps/api/core";
import type { AppConfig } from "@/types";
export const configApi = {
getAll: () => invoke<AppConfig[]>("get_all_config"),
get: (key: string) => invoke<string>("get_config", { key }),
set: (key: string, value: string) =>
invoke<void>("set_config", { key, value }),
delete: (key: string) => invoke<void>("delete_config", { key }),
};
try {
const configs = await configApi.getAll();
} catch (error) {
message.error(`获取配置失败: ${error}`);
}
3. ErrorBoundary 组件(Ant Design Result)
import { Component, ReactNode } from "react";
import { Result, Button } from "antd";
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error: Error | null;
}
export class ErrorBoundary extends Component<Props, State> {
state: State = { hasError: false, error: null };
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: any) {
console.error("ErrorBoundary 捕获错误:", error, errorInfo);
}
render() {
if (this.state.hasError) {
return this.props.fallback || (
<Result
status="error"
title="应用出现错误"
subTitle={this.state.error?.message}
extra={
<Button type="primary" onClick={() => window.location.reload()}>
刷新页面
</Button>
}
/>
);
}
return this.props.children;
}
}
4. 全局错误处理 Hook
import { useState } from "react";
import { message } from "antd";
import { invoke } from "@tauri-apps/api/core";
export function useErrorHandler() {
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
async function safeInvoke<T>(
cmd: string,
args?: Record<string, unknown>,
showSuccessMsg?: string
): Promise<T | null> {
setLoading(true);
setError(null);
try {
const result = await invoke<T>(cmd, args);
if (showSuccessMsg) {
message.success(showSuccessMsg);
}
return result;
} catch (e) {
const msg = String(e);
setError(msg);
message.error(msg);
console.error(`Command "${cmd}" 失败:`, msg);
return null;
} finally {
setLoading(false);
}
}
return {
error,
loading,
safeInvoke,
clearError: () => setError(null),
};
}
const { loading, safeInvoke } = useErrorHandler();
async function handleSave() {
const result = await safeInvoke<string>(
"set_config",
{ key: "theme", value: "dark" },
"保存成功"
);
if (result) {
}
}
错误流程图
┌─────────────────────────────────────────────────────────────┐
│ Rust 后端 │
├─────────────────────────────────────────────────────────────┤
│ Database::get_config() │
│ ↓ 返回 Result<Option<String>, AppError> │
│ Service::get_required() │
│ ↓ 业务校验,转换 None 为 NotFound 错误 │
│ Command::get_config() │
│ ↓ map_err(|e| e.to_string()) 转换为 String │
└─────────────────────────────────────────────────────────────┘
↓ IPC (invoke)
┌─────────────────────────────────────────────────────────────┐
│ React 前端 │
├─────────────────────────────────────────────────────────────┤
│ try { await configApi.get("theme") } │
│ catch (error) { message.error(String(error)) } │
│ ↓ 用户看到 Ant Design 错误提示 │
└─────────────────────────────────────────────────────────────┘
常见错误
| 错误做法 | 正确做法 |
|---|
Rust 中 unwrap() 处理可能失败的操作 | 使用 ? 运算符 + Result<T, AppError> |
| 不定义统一错误类型 | 使用 thiserror 定义 AppError 枚举 |
| 前端不 catch invoke 错误 | 所有 invoke 调用都用 try-catch |
| 错误信息不可读 | 提供用户友好的中文错误提示 |
Mutex 使用 unwrap() | 使用 map_err 转换为 AppError::Custom |
前端用 alert() 显示错误 | 使用 Ant Design message.error() |
| 不处理 ErrorBoundary | 在根组件添加 <ErrorBoundary> |
完整示例(三层 + 前端)
Rust 后端
#[derive(Debug, Error)]
pub enum AppError {
#[error("数据库错误: {0}")]
Database(#[from] rusqlite::Error),
#[error("未找到: {0}")]
NotFound(String),
}
impl Database {
pub fn get_user(&self, id: i64) -> Result<Option<User>, AppError> {
let conn = self.conn.lock()
.map_err(|e| AppError::Custom(e.to_string()))?;
}
}
impl UserService {
pub fn get_required(&self, db: &Database, id: i64) -> Result<User, AppError> {
db.get_user(id)?
.ok_or_else(|| AppError::NotFound(format!("用户 {} 不存在", id)))
}
}
#[tauri::command]
pub fn get_user(db: State<'_, Database>, id: i64) -> Result<User, String> {
let service = UserService::new();
service.get_required(&db, id)
.map_err(|e| e.to_string())
}
React 前端
import { message } from "antd";
import { invoke } from "@tauri-apps/api/core";
async function loadUser(id: number) {
try {
const user = await invoke<User>("get_user", { id });
setUser(user);
} catch (error) {
message.error(`加载用户失败: ${error}`);
}
}