Skip to main content

Error Handling in Async Dioxus

Dioxus provides powerful error handling mechanisms that make it easy to catch, display, and recover from errors in your application.

ErrorBoundary Component

The ErrorBoundary component catches errors from child components and renders a custom error UI:
use dioxus::prelude::*;

fn app() -> Element {
    rsx! {
        ErrorBoundary {
            handle_error: |error: ErrorContext| rsx! {
                div { class: "error",
                    h1 { "An error occurred" }
                    pre { "{error:#?}" }
                }
            },
            MyComponent {}
        }
    }
}

How It Works

Dioxus automatically catches errors from:
  • Rendering - Errors returned from component functions
  • Event handlers - Errors returned from event handlers
  • Async operations - Errors from futures and resources
  • Panics - Panics in components (except in WASM)

Returning Errors

Components and handlers can return Result<Element, E> where E implements Into<anyhow::Error>:
#[component]
fn MyComponent() -> Element {
    let data = fetch_data()?; // Returns early on error
    
    rsx! {
        div { "Data: {data}" }
    }
}

fn fetch_data() -> anyhow::Result<String> {
    // Returns Result<String, anyhow::Error>
    Ok("data".to_string())
}

Event Handler Errors

Event handlers can return Result<(), Error>:
fn app() -> Element {
    rsx! {
        button {
            onclick: move |_| {
                let value: i32 = "not a number".parse()?;
                println!("Parsed: {value}");
                Ok(())
            },
            "Click to trigger error"
        }
    }
}

The bail! Macro

Use the bail! macro to return early with an error:
use dioxus::prelude::*;

fn validate_age(age: i32) -> Element {
    if age < 0 {
        bail!("Age cannot be negative");
    }
    if age > 150 {
        bail!("Age seems unrealistic: {}", age);
    }
    
    rsx! { "Valid age: {age}" }
}

ErrorContext API

The ErrorContext provides information about caught errors:
ErrorBoundary {
    handle_error: |context: ErrorContext| {
        let error = context.error();
        
        rsx! {
            div {
                h2 { "Something went wrong" }
                if let Some(err) = error {
                    pre { "{err}" }
                }
            }
        }
    },
    Content {}
}

Methods

  • error() - Get the current error (returns Option<CapturedError>)
  • clear_errors() - Clear all errors and retry rendering

Resetting Error Boundaries

You can clear errors and retry rendering:
fn app() -> Element {
    let mut input = use_signal(|| String::new());
    
    rsx! {
        input {
            value: "{input}",
            oninput: move |e| input.set(e.value())
        }
        
        ErrorBoundary {
            handle_error: |error_context| rsx! {
                div { class: "error",
                    "Error: {error_context.error():?}"
                    button {
                        onclick: move |_| {
                            // Clear the error and retry
                            error_context.clear_errors();
                        },
                        "Try again"
                    }
                }
            },
            ParseNumber { input: input() }
        }
    }
}

#[component]
fn ParseNumber(input: String) -> Element {
    let number: i32 = input.parse()?;
    rsx! { "Parsed number: {number}" }
}

Error Type Checking

You can downcast errors to specific types for custom handling:
use std::num::ParseIntError;

ErrorBoundary {
    handle_error: |context: ErrorContext| {
        if let Some(error) = context.error() {
            // Check for specific error type
            if let Some(parse_error) = error.downcast_ref::<ParseIntError>() {
                return rsx! {
                    div { class: "parse-error",
                        "Invalid number format"
                        p { "Please enter a valid integer" }
                    }
                };
            }
        }
        
        // Default error display
        rsx! {
            div { class: "error",
                "An unexpected error occurred"
            }
        }
    },
    Content {}
}

Multiple Error Types

Iterate over all errors:
ErrorBoundary {
    handle_error: |context: ErrorContext| rsx! {
        div { class: "errors",
            h2 { "Errors occurred:" }
            for error in context.error() {
                div { class: "error-item",
                    if let Some(parse_err) = error.downcast_ref::<ParseIntError>() {
                        div { 
                            background_color: "red",
                            "Parse error: {parse_err}"
                        }
                    } else {
                        div { "{error}" }
                    }
                }
            }
        }
    },
    Content {}
}

Catching Panics

On desktop platforms, Dioxus catches panics automatically:
#[component]
fn PanickingComponent() -> Element {
    panic!("This component panics!");
}

fn app() -> Element {
    rsx! {
        ErrorBoundary {
            handle_error: |error| rsx! {
                div { "Caught a panic: {error:#?}" }
            },
            PanickingComponent {}
        }
    }
}
Note: In WASM, panics cannot currently be caught by error boundaries due to browser limitations.

Async Error Handling

With use_resource

fn app() -> Element {
    let data = use_resource(|| async move {
        reqwest::get("https://api.example.com/data")
            .await?
            .json::<MyData>()
            .await
    });
    
    match &*data.read_unchecked() {
        Some(Ok(value)) => rsx! { "Success: {value}" },
        Some(Err(err)) => rsx! { 
            div { class: "error",
                "Failed to fetch data: {err}"
            }
        },
        None => rsx! { "Loading..." },
    }
}

With Suspense

Combine ErrorBoundary with SuspenseBoundary:
fn app() -> Element {
    rsx! {
        ErrorBoundary {
            handle_error: |err| rsx! { "Error: {err}" },
            SuspenseBoundary {
                fallback: |_| rsx! { "Loading..." },
                AsyncContent {}
            }
        }
    }
}

#[component]
fn AsyncContent() -> Element {
    let data = use_resource(|| async move {
        fetch_data().await // Can fail
    });
    
    let data = data.suspend()?; // Throws error if fetch failed
    
    rsx! { "Data: {data}" }
}

Error Propagation

Errors bubble up to the nearest error boundary:
fn app() -> Element {
    rsx! {
        // Outer boundary catches all errors
        ErrorBoundary {
            handle_error: |err| rsx! { "Top-level error: {err}" },
            
            // Inner components can throw errors
            Level1 {}
        }
    }
}

#[component]
fn Level1() -> Element {
    rsx! {
        Level2 {}
    }
}

#[component]
fn Level2() -> Element {
    // This error propagates up to the ErrorBoundary
    bail!("Error from Level2");
}

Nested Error Boundaries

Use multiple boundaries for granular error handling:
fn app() -> Element {
    rsx! {
        // Global error boundary
        ErrorBoundary {
            handle_error: |err| rsx! {
                div { class: "critical-error",
                    "Critical application error: {err}"
                }
            },
            
            div {
                h1 { "My App" }
                
                // User section with its own error handling
                ErrorBoundary {
                    handle_error: |err| rsx! {
                        div { class: "user-error",
                            "Failed to load user: {err}"
                        }
                    },
                    UserProfile {}
                }
                
                // Posts section with separate error handling
                ErrorBoundary {
                    handle_error: |err| rsx! {
                        div { class: "posts-error",
                            "Failed to load posts: {err}"
                        }
                    },
                    PostsList {}
                }
            }
        }
    }
}

Custom Error Types

Create custom error types for your application:
use thiserror::Error;

#[derive(Error, Debug)]
enum AppError {
    #[error("Network error: {0}")]
    Network(#[from] reqwest::Error),
    
    #[error("Parse error: {0}")]
    Parse(#[from] std::num::ParseIntError),
    
    #[error("Not found: {0}")]
    NotFound(String),
    
    #[error("Unauthorized")]
    Unauthorized,
}

fn fetch_user(id: i32) -> Result<User, AppError> {
    if id < 0 {
        return Err(AppError::NotFound(format!("User {id}")));
    }
    // ...
    Ok(User { id, name: "Alice".to_string() })
}

#[component]
fn UserComponent(id: i32) -> Element {
    let user = fetch_user(id)?;
    
    rsx! {
        div { "User: {user.name}" }
    }
}
Then handle them specifically:
ErrorBoundary {
    handle_error: |context| {
        if let Some(error) = context.error() {
            if let Some(app_err) = error.downcast_ref::<AppError>() {
                return match app_err {
                    AppError::Network(e) => rsx! {
                        div { "Network issue: {e}. Please check your connection." }
                    },
                    AppError::NotFound(item) => rsx! {
                        div { "{item} not found" }
                    },
                    AppError::Unauthorized => rsx! {
                        div { "Please log in to continue" }
                    },
                    _ => rsx! { "An error occurred: {app_err}" },
                };
            }
        }
        rsx! { "Unknown error" }
    },
    MyApp {}
}

Best Practices

1. Use Specific Error Types

Define clear error types for different parts of your app:
#[derive(Error, Debug)]
enum UserError {
    #[error("User not found")]
    NotFound,
    #[error("Invalid user data")]
    InvalidData,
}

#[derive(Error, Debug)]
enum PaymentError {
    #[error("Payment declined")]
    Declined,
    #[error("Insufficient funds")]
    InsufficientFunds,
}

2. Provide Context

Add context to errors for better debugging:
use anyhow::Context;

fn load_config() -> anyhow::Result<Config> {
    let contents = std::fs::read_to_string("config.toml")
        .context("Failed to read config file")?;
    
    let config: Config = toml::from_str(&contents)
        .context("Failed to parse config")?;
    
    Ok(config)
}

3. User-Friendly Error Messages

Show helpful messages to users:
ErrorBoundary {
    handle_error: |context| rsx! {
        div { class: "error-card",
            h2 { "Oops! Something went wrong" }
            p { "We're having trouble loading this page." }
            button {
                onclick: move |_| context.clear_errors(),
                "Try Again"
            }
            details {
                summary { "Technical details" }
                pre { "{context.error():?}" }
            }
        }
    },
    Content {}
}

4. Log Errors

Log errors for monitoring:
ErrorBoundary {
    handle_error: |context| {
        if let Some(error) = context.error() {
            tracing::error!("Component error: {:?}", error);
            // Or send to error tracking service
        }
        rsx! { "An error occurred" }
    },
    Content {}
}

5. Granular Boundaries

Use multiple boundaries to isolate failures:
rsx! {
    // Each section can fail independently
    ErrorBoundary { handle_error: |e| rsx! { "Header error" }, Header {} }
    ErrorBoundary { handle_error: |e| rsx! { "Content error" }, Content {} }
    ErrorBoundary { handle_error: |e| rsx! { "Footer error" }, Footer {} }
}

Complete Example

Here’s a complete example showing error handling with async data:
use dioxus::prelude::*;
use thiserror::Error;

#[derive(Error, Debug, Clone)]
enum ApiError {
    #[error("Network error")]
    Network,
    #[error("Not found")]
    NotFound,
    #[error("Server error")]
    Server,
}

fn app() -> Element {
    rsx! {
        ErrorBoundary {
            handle_error: |context| rsx! {
                div { class: "error-container",
                    h1 { "Error" }
                    p { "{context.error():?}" }
                    button {
                        onclick: move |_| context.clear_errors(),
                        "Retry"
                    }
                }
            },
            SuspenseBoundary {
                fallback: |_| rsx! { "Loading user..." },
                UserProfile { user_id: 1 }
            }
        }
    }
}

#[component]
fn UserProfile(user_id: i32) -> Element {
    let user = use_resource(move || async move {
        fetch_user(user_id).await
    });
    
    let user = user.suspend()?;
    
    rsx! {
        div {
            h2 { "{user.name}" }
            p { "{user.email}" }
        }
    }
}

async fn fetch_user(id: i32) -> Result<User, ApiError> {
    // Simulated API call
    if id < 0 {
        return Err(ApiError::NotFound);
    }
    Ok(User {
        name: "Alice".to_string(),
        email: "[email protected]".to_string(),
    })
}

struct User {
    name: String,
    email: String,
}

Next Steps

Build docs developers (and LLMs) love