leptos_shadcn_error_boundary/
lib.rs

1//! Production-Ready Error Handling for Leptos
2//! 
3//! This module provides simple, effective error handling for production applications.
4//! It focuses on graceful degradation and user experience rather than complex error boundaries.
5
6use leptos::prelude::*;
7use std::panic::PanicHookInfo;
8
9/// Simple error information for production use
10#[derive(Clone, Debug)]
11pub struct ErrorInfo {
12    /// User-friendly error message
13    pub message: String,
14    /// Technical error details (for logging)
15    pub technical_details: Option<String>,
16}
17
18/// Simple error fallback component
19#[component]
20pub fn ErrorFallback(
21    #[prop(into)] error_info: ErrorInfo,
22) -> impl IntoView {
23    view! {
24        <div class="error-fallback">
25            <div class="error-content">
26                <div class="error-icon">!</div>
27                <h2 class="error-title">Something went wrong</h2>
28                <p class="error-message">
29                    {error_info.message}
30                </p>
31                <div class="error-actions">
32                    <button 
33                        class="error-retry"
34                        on:click=move |_| {
35                            // Simple page reload for now
36                            if let Some(window) = web_sys::window() {
37                                let _ = window.location().reload();
38                            }
39                        }
40                    >
41                        "Try Again"
42                    </button>
43                </div>
44            </div>
45        </div>
46    }
47}
48
49/// Simple error boundary wrapper
50#[component]
51pub fn ErrorBoundary(
52    #[prop(into)] children: Children,
53) -> impl IntoView {
54    let (has_error, set_has_error) = signal(false);
55    let (_error_info, set_error_info) = signal(None::<ErrorInfo>);
56    
57    // Set up panic hook for production error handling
58    std::panic::set_hook(Box::new(move |panic_info: &PanicHookInfo<'_>| {
59        log::error!("Panic caught: {:?}", panic_info);
60        
61        let error = ErrorInfo {
62            message: "An unexpected error occurred. Please try refreshing the page.".to_string(),
63            technical_details: Some(format!("{:?}", panic_info)),
64        };
65        
66        set_error_info.set(Some(error));
67        set_has_error.set(true);
68    }));
69    
70    // Render children or error fallback using a different approach
71    if has_error.get() {
72        if let Some(error) = _error_info.get() {
73            view! { <ErrorFallback error_info=error /> }.into_any()
74        } else {
75            view! { <ErrorFallback error_info=ErrorInfo { message: "An error occurred".to_string(), technical_details: None } /> }.into_any()
76        }
77    } else {
78        children()
79    }
80}
81
82/// Hook for manual error handling
83pub fn use_error_handler() -> (ReadSignal<bool>, WriteSignal<bool>, WriteSignal<Option<ErrorInfo>>) {
84    let (has_error, set_has_error) = signal(false);
85    let (_error_info, set_error_info) = signal(None::<ErrorInfo>);
86    
87    (has_error, set_has_error, set_error_info)
88}
89
90/// Utility function to create user-friendly error messages
91pub fn create_user_error(message: &str, technical: Option<&str>) -> ErrorInfo {
92    ErrorInfo {
93        message: message.to_string(),
94        technical_details: technical.map(|s| s.to_string()),
95    }
96}
97
98/// Utility function to handle errors gracefully
99pub fn handle_error<T>(result: Result<T, impl std::fmt::Debug>) -> Option<T> {
100    match result {
101        Ok(value) => Some(value),
102        Err(error) => {
103            log::error!("Error occurred: {:?}", error);
104            None
105        }
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn test_error_info_creation() {
115        let error = ErrorInfo {
116            message: "Test error".to_string(),
117            technical_details: Some("Technical details".to_string()),
118        };
119        
120        assert_eq!(error.message, "Test error");
121        assert_eq!(error.technical_details, Some("Technical details".to_string()));
122    }
123
124    #[test]
125    fn test_create_user_error() {
126        let error = create_user_error("User message", Some("Technical"));
127        assert_eq!(error.message, "User message");
128        assert_eq!(error.technical_details, Some("Technical".to_string()));
129    }
130
131    #[test]
132    fn test_handle_error() {
133        let result: Result<i32, &str> = Ok(42);
134        assert_eq!(handle_error(result), Some(42));
135        
136        let error_result: Result<i32, &str> = Err("Error");
137        assert_eq!(handle_error(error_result), None);
138    }
139
140    #[test]
141    fn test_error_info_without_technical_details() {
142        let error = ErrorInfo {
143            message: "Simple error".to_string(),
144            technical_details: None,
145        };
146        
147        assert_eq!(error.message, "Simple error");
148        assert_eq!(error.technical_details, None);
149    }
150
151    #[test]
152    fn test_create_user_error_without_technical() {
153        let error = create_user_error("User message", None);
154        assert_eq!(error.message, "User message");
155        assert_eq!(error.technical_details, None);
156    }
157
158    #[test]
159    fn test_use_error_handler() {
160        let (has_error, set_has_error, set_error_info) = use_error_handler();
161        
162        // Initially no error
163        assert!(!has_error.get());
164        
165        // Set an error
166        let error = ErrorInfo {
167            message: "Test error".to_string(),
168            technical_details: None,
169        };
170        set_error_info.set(Some(error));
171        set_has_error.set(true);
172        
173        // Check error is set
174        assert!(has_error.get());
175    }
176
177    #[test]
178    fn test_error_info_clone_and_debug() {
179        let error = ErrorInfo {
180            message: "Test error".to_string(),
181            technical_details: Some("Technical".to_string()),
182        };
183        
184        let cloned = error.clone();
185        assert_eq!(error.message, cloned.message);
186        assert_eq!(error.technical_details, cloned.technical_details);
187        
188        // Test debug formatting
189        let debug_str = format!("{:?}", error);
190        assert!(debug_str.contains("Test error"));
191        assert!(debug_str.contains("Technical"));
192    }
193}