Tauri error handling recipes
Pavlo Myroniuk December 26, 2024 #recipes #rust #tauri #leptosIntro
I'm developing the desktop app to meet my own needs: Dataans.
Take notes in the form of markdown snippets grouped into spaces.
I use Tauri as the main framework and Leptos as the frontend. I am satisfied with the developing experience and the result I got.
During the initial implementation (PoC phase), I didn't implement any error handling and called .unwrap()
every time. However, after the first release, I needed to add proper error handling and make the app more fault-tolerant. The refactoring process wasn't easy and I spent more time than expected. I learned some important lessons and decided to write this article to summarize my knowledge.
Disclaimer: I have experience using Tauri
only with Leptos
and other Rust-based frontend frameworks. Error handling approaches may be different when using JS-based frontend frameworks.
Set up
I set up a simple project (basically it is a cargo create-tauri-app
with minimal changes) to demonstrate the approach and the progress: github/TheBestTvarynka/trash-code/265f39bfde2aa9fb10055bfee5031714498f7b28/tauri-error-handling. I left a corresponding commit link for each section in this article. So, you can follow this process and even reproduce it with me.
So, what do we have? We have one simple Tauri command:
// Backend: src-tauri/src/lib.rs
// Frontend: src/backend.rs
pub async
Let's handle the error
Usually, most of the commands may fail. We may want to return a Result
and handle the error on the frontend side. Let's try it. Suppose we need to validate the name. I came up with the following implementation (let's keep it simple):
use Error;
Unfortunately, now we have a compilation error:
error[E0599]: the method `blocking_kind` exists for reference `&Result<String, Error>`, but its trait bounds were not satisfied
--> src-tauri/src/lib.rs:14:1
|
5 | enum Error {
| ---------- doesn't satisfy `Error: Into<InvokeError>`
...
14 | #[tauri::command]
| ^^^^^^^^^^^^^^^^^ method cannot be called on `&Result<String, Error>` due to unsatisfied trait bounds
...
25 | .invoke_handler(tauri::generate_handler![greet])
| ------------------------------- in this macro invocation
|
::: /home/pavlo-myroniuk/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:527:1
|
527 | pub enum Result<T, E> {
| --------------------- doesn't satisfy `Result<std::string::String, Error>: IpcResponse` or `_: ResultKind`
|
= note: the following trait bounds were not satisfied:
`Error: Into<InvokeError>`
which is required by `Result<std::string::String, Error>: tauri::ipc::private::ResultKind`
`Result<std::string::String, Error>: IpcResponse`
which is required by `&Result<std::string::String, Error>: tauri::ipc::private::ResponseKind`
🤔. Our Result<String, Error>
type must implement the IpcResponse
trait. Implementing serde::Serialize
should be enough (because of the impl<T: Serialize> IpcResponse for T
trait bound. docs).
Great. Compilation successful. Alternatively, you can read another explanation and example: The Tauri Documentation WIP/Inter-Process Communication#error-handling.
Lesson 1. All Error
s must implement the serde::Serialize
trait.
What about frontend? How are we going to handle the error? Let's start with the simplest solution:
- Move the
Error
type to a separatecommon
crate which can be used by frontend and backend. - Expect
JsValue
to be parsed intoResult<String, Error>
.
pub async
Oops, we have a problem in runtime:
But why � According to the documentation (v2.tauri.app/calling-rust/#error-handling), Tauri command error will be an exception on frontend 😕. So, we should not expect the Result<String, Error>
, but handle a JS exception instead.
Fortunately, we can ask wasm_bindgen
to handle this exception. Tauri docs say nothing about it, but if we open the wasm-bindgen documentation instead, we will find this treasure (wasm-bindgen/attributes/on-js-imports/catch):
The
catch
attribute allows catching a JavaScript exception. This can be attached to any imported function or method, and the function must return aResult
where theErr
payload is aJsValue
:
😲. Let's use it. Now I'm going to change the binding generated by cargo create-tauri-app
.
extern "C"
And now we can handle the exception as an error:
pub async
And finally, it works 🎉. Here is the resulting code: github/TheBestTvarynka/trash-code/9f6437fe7799e87439430e483c92c0ed590b12f3/tauri-error-handling.
Lesson 2. Tauri command error is an exception on frontend. The exception can be caught by using the catch
attribute of the #[wasm_bindgen]
macro.
Bonus: unexpected None
As you can see from the invoke
function declaration, command input/output values are serialized into/deserialized from JsValue using the serde_wasm_bindgen
crate. But we are also aware that the JS type-system and Rust type-system are different. Thus, not all Rust objects can be represented in a JS type-system. Theoretically, we can find a Rust object, that:
obj == from_value.unwrap
// false
I mean, objects before and after the serialization + deserialization process are not the same. It was theoretical until yesterday when I faced it in practice 🤪. Suppose we have the following Tauri command:
And the corresponding code on frontend side:
pub async
Let's try it.
Oppps 😯. We have None
instead of Some(())
. Some(())
is serialized in undefined
and undefined
is deserialized in None
. You should be very careful with ()
and enum
s. You can reproduce it by yourself: github/TheBestTvarynka/trash-code/4a4f62d1d55687cc02538408ceb9eb6fee93187f/tauri-error-handling.
Lesson 3. Objects before and after the serialization + deserialization process may be different. Be careful when using types like ()
, enum
s, etc.
If you say that no one uses the Option<()>
type, then I will disagree. Try to search for Option<()>
on GitHub and you will find many uses of this type: github/search?q=Option%3C%28%29%3E+lang%3ARust+&type=code.
Learned lessons
- All
Error
s must implement theserde::Serialize
trait. - Tauri command error (
Result::Err(E)
) is an exception on frontend. The exception can be caught by using thecatch
attribute of the#[wasm_bindgen]
macro. - The JS type-system and Rust type-system are different. Objects before and after the serialization + deserialization process may be different.
Fun fact. This article looks nothing like the one I planned. I didn't know about the catch
attribute before writing the article. I discovered it accidentally 🤪. I even wrote my own Result
and Unit
types to implement proper error handling: github/TheBestTvarynka/Dataans/3f2b97e81cbe8b6c4cd0fffc6d0b44c0a8c4e748/dataans/common/src/error.rs (but now I plan to delete them).
> refactored error handling in my Tauri project
> it turned out harder than I thought
> decided to write a blog post about error handling in Tauri
> while writing the blog post I found out that I could have done it much easier
> now I am going to refactor the error handling again
I like this side effect of writing articles. Most likely, you will do some research, investigation, and code examples to better explain your idea. Almost every time I write a blog post I discover something new for me.
- Writing blog posts is a good way to learn something more deeply 🤩.