Error Handling
In the last chapter, we saw that you can render Option<T>:
in the None case, it will render nothing, and in the Some(T) case, it will render T
(that is, if T implements IntoView). You can actually do something very similar
with a Result<T, E>. In the Err(_) case, it will render nothing. In the Ok(T)
case, it will render the T.
Let’s start with a simple component to capture a number input.
#[component]
fn NumericInput() -> impl IntoView {
let (value, set_value) = signal(Ok(0));
view! {
<label>
"Type an integer (or not!)"
<input type="number" on:input:target=move |ev| {
// when input changes, try to parse a number from the input
set_value.set(ev.target().value().parse::<i32>())
}/>
<p>
"You entered "
<strong>{value}</strong>
</p>
</label>
}
}
Every time you change the input, on_input will attempt to parse its value into a 32-bit
integer (i32), and store it in our value signal, which is a Result<i32, _>. If you
type the number 42, the UI will display
You entered 42
But if you type the string foo, it will display
You entered
This is not great. It saves us using .unwrap_or_default() or something, but it would be
much nicer if we could catch the error and do something with it.
You can do that, with the <ErrorBoundary/>
component.
People often try to point out that <input type="number"> prevents you from typing a string
like foo, or anything else that's not a number. This is true in some browsers, but not in all!
Moreover, there are a variety of things that can be typed into a plain number input that are not an
i32: a floating-point number, a larger-than-32-bit number, the letter e, and so on. The browser
can be told to uphold some of these invariants, but browser behavior still varies: Parsing for yourself
is important!
<ErrorBoundary/>
An <ErrorBoundary/> is a little like the <Show/> component we saw in the last chapter.
If everything’s okay—which is to say, if everything is Ok(_)—it renders its children.
But if there’s an Err(_) rendered among those children, it will trigger the
<ErrorBoundary/>’s fallback.
Let’s add an <ErrorBoundary/> to this example.
#[component]
fn NumericInput() -> impl IntoView {
let (value, set_value) = signal(Ok(0));
view! {
<h1>"Error Handling"</h1>
<label>
"Type a number (or something that's not a number!)"
<input type="number" on:input:target=move |ev| {
// when input changes, try to parse a number from the input
set_value.set(ev.target().value().parse::<i32>())
}/>
// If an `Err(_) had been rendered inside the <ErrorBoundary/>,
// the fallback will be displayed. Otherwise, the children of the
// <ErrorBoundary/> will be displayed.
<ErrorBoundary
// the fallback receives a signal containing current errors
fallback=|errors| view! {
<div class="error">
<p>"Not a number! Errors: "</p>
// we can render a list of errors
// as strings, if we'd like
<ul>
{move || errors.get()
.into_iter()
.map(|(_, e)| view! { <li>{e.to_string()}</li>})
.collect::<Vec<_>>()
}
</ul>
</div>
}
>
<p>
"You entered "
// because `value` is `Result<i32, _>`,
// it will render the `i32` if it is `Ok`,
// and render nothing and trigger the error boundary
// if it is `Err`. It's a signal, so this will dynamically
// update when `value` changes
<strong>{value}</strong>
</p>
</ErrorBoundary>
</label>
}
}
Now, if you type 42, value is Ok(42) and you’ll see
You entered 42
If you type foo, value is Err(_) and the fallback will render. We’ve chosen to render
the list of errors as a String, so you’ll see something like
Not a number! Errors:
- cannot parse integer from empty string
If you fix the error, the error message will disappear and the content you’re wrapping in
an <ErrorBoundary/> will appear again.
Live example
CodeSandbox Source
use leptos::prelude::*;
#[component]
fn App() -> impl IntoView {
let (value, set_value) = signal(Ok(0));
view! {
<h1>"Error Handling"</h1>
<label>
"Type a number (or something that's not a number!)"
<input type="number" on:input:target=move |ev| {
// when input changes, try to parse a number from the input
set_value.set(ev.target().value().parse::<i32>())
}/>
// If an `Err(_) had been rendered inside the <ErrorBoundary/>,
// the fallback will be displayed. Otherwise, the children of the
// <ErrorBoundary/> will be displayed.
<ErrorBoundary
// the fallback receives a signal containing current errors
fallback=|errors| view! {
<div class="error">
<p>"Not a number! Errors: "</p>
// we can render a list of errors
// as strings, if we'd like
<ul>
{move || errors.get()
.into_iter()
.map(|(_, e)| view! { <li>{e.to_string()}</li>})
.collect::<Vec<_>>()
}
</ul>
</div>
}
>
<p>
"You entered "
// because `value` is `Result<i32, _>`,
// it will render the `i32` if it is `Ok`,
// and render nothing and trigger the error boundary
// if it is `Err`. It's a signal, so this will dynamically
// update when `value` changes
<strong>{value}</strong>
</p>
</ErrorBoundary>
</label>
}
}
fn main() {
leptos::mount::mount_to_body(App)
}