Open usagrada opened 1 year ago
Yeah so the Error
type is neither public nor very stable right now. But I get the use case. Implementing an ReceivedError
such as you proposed seems a bit redundant since tauri_sys::Error
will cover all these cases and be more flexible. I'll try to prioritise fixing up the error enum. Maybe you are also willing to improve the error messages, feel free to open a PR in that case!
Thank you for comment. Right, this implementation is redundant and I also think that covering all these cases in tauri_sys::Error
is better way. Fixing up the error enum is very helpful for me!
The error type in simple example is String, but when that is custom type like struct, I feared that changing tauri_sys::Error
would have a widespread effect because many files uses this type.
When some good implementation, I will comment in this thread and will open PR.
Wait a second, maybe I was misunderstanding. Your feature request is basically being able to have structured errors on the Core side that can be interpreted as structured errors on the client too?
So like this: Core:
#[derive(Serialize)]
struct Error {
code: u32,
message: String
}
#[tauri::command]
fn cmd() -> Result<(), Error> {
todo!();
}
Client:
#[derive(Deserialize)]
struct Error {
code: u32,
message: String
}
let res = invoke::<(), Error>("cmd").await?;
Yes! Sorry that my example is bad for understanding. Your example represents my thoughts correctly. I want to handle not only String type but also custom type error in client side.
Okay yeah but that actually does not require any changes to tauri-sys
afaik. What you can do is the following:
shared
or types
or whateverserde::Serialize
and serde::Deserialize
but ideally you also use a crate for nicer error handling such as thiserror
invoke
like this:
let res = invoke::<(), Result<T, Error>>("command").await;
match res { Ok(res) => match res { Ok(t) => println!("command returned successful {}", t), Err(e) => println!("command returned error {:?}", e) } Err(e) => panic!("Some issue with deserialising or Tauri") }
or a bit shorter using `?`
```rust
let res = invoke::<(), Result<T, Error>>("command").await?; // use ? here to bubble up the tauri-sys errors
Ok(res) => match res {
Ok(t) => println!("command returned successful {}", t),
Err(e) => println!("command returned error {:?}", e)
}
I appreciate your advice, but your code did not work in my environments(My setting is wrong?).
When command returned error, res
is Err(Error::Binding(~))
not nested Result
type value.
Could you check this problem?
let res = invoke::<(), Result<T, Error>>("command").await;
match res {
Ok(res) => match res {
Ok(t) => println!("command returned successful {}", t),
Err(e) => println!("command returned error {:?}", e)
}
Err(e) => panic!("Some issue with deserialising or Tauri") // matching now
}
Okay yeah so this is down to how the JSON serialisation and invoke
kinda "flattens" results, not much we can do about this immediately :/ but making tauri_sys::Error
public should allow you to differentiate at least. Maybe the binding variant can keep the JsValue
for you to deserialise manually or something
I see…… I agree that making tauri_sys::Error public is a quick fix for this problem.
As extending tauri_sys::Error
, I re-implemented. It also requires making tauri_sys::Error
public. How is it?
url
Okay yeah so this is down to how the JSON serialisation and
invoke
kinda "flattens" results, not much we can do about this immediately :/ but makingtauri_sys::Error
public should allow you to differentiate at least. Maybe the binding variant can keep theJsValue
for you to deserialise manually or something
The following worked perfectly for me after making the error module public:
Shared crate:
#[derive(Error, Debug, Clone, Serialize, Deserialize)]
pub enum Error {
#[error("unauthorized user")]
UnauthorizedUserError,
#[error("unknown error")]
UnknownError,
#[error("wrapped tauri-sys::Error error")]
TauriSysError(String),
#[error("not a tauri-sys::Error::Binding error")]
NotABindingError,
#[error("tauri-sys::Error::Binding deserialization error")]
BindingDeserializationError,
}
impl Error {
pub fn from_binding(value: String) -> Self {
let re = Regex::new(r#"JsValue\((?P<error>"\w+")\)"#).unwrap();
match re.captures(&value) {
Some(caps) => match caps["error"].parse::<String>() {
Ok(str) => match serde_json::from_str::<Error>(str.as_str()) {
Ok(error) => error,
_ => Error::BindingDeserializationError,
},
_ => unreachable!(),
},
_ => Error::NotABindingError,
}
}
}
Backend:
#[tauri::command]
pub async fn sign_in(email: String, password: String) -> Result<UserInfoDTO, Error> {
...
match response_info.status {
200 => {
Ok(UserInfoDTO { signed_in: true })
}
_ => Err(Error::UnauthorizedUserError),
}
}
Frontend:
pub async fn sign_in_command(email: String, password: String) -> Result<UserInfoDTO, Error> {
let res =
tauri::invoke::<SignInCmdArgs, UserInfoDTO>("sign_in", &SignInCmdArgs { email, password })
.await;
handle_backend_result::<UserInfoDTO>(res)
}
fn handle_backend_result<T>(result: Result<T, tauri_sys::error::Error>) -> Result<T, Error> {
match result {
Ok(result) => Ok(result),
Err(tauri_sys::error::Error::Binding(string)) => Err(Error::from_binding(string)),
Err(e) => Err(Error::TauriSysError(e.to_string())),
}
}
Thank you for advice. Your code maybe works truly, but I don't think users handling library error is good idea. I think adding third parameter in generics for error type is better idea (These codes become panic when users pass wrong type, but work truly when users pass same type in frontend and backend.).
type Response<T, E> = Result<T, E>;
#[inline(always)]
pub async fn invoke<A: Serialize, R: DeserializeOwned, E: DeserializeOwned>(
cmd: &str,
args: &A,
) -> Response<R, E> {
let raw = inner::invoke(
cmd,
serde_wasm_bindgen::to_value(args).expect("serde binding error: args"),
)
.await;
match raw {
Ok(raw) => {
let res: R = serde_wasm_bindgen::from_value(raw).expect("serde binding error");
Ok(res)
}
Err(e) => {
let error: E = serde_wasm_bindgen::from_value(e).expect("serde binding error");
Err(error)
}
}
}
// error.rs
use serde::de::DeserializeOwned;
use wasm_bindgen::JsValue;
#[derive(Clone, Eq, PartialEq, Debug, thiserror::Error)]
pub enum Error<T: DeserializeOwned = String> {
#[error("TODO.")]
Binding(String),
#[error("TODO.")]
Serde(String),
#[cfg(any(feature = "event", feature = "window"))]
#[error("TODO.")]
OneshotCanceled(#[from] futures::channel::oneshot::Canceled),
#[error("custom error TODO.")]
CustomError(T),
}
impl<T: DeserializeOwned> From<serde_wasm_bindgen::Error> for Error<T> {
fn from(e: serde_wasm_bindgen::Error) -> Self {
Self::Serde(e.to_string())
}
}
impl<T: DeserializeOwned> From<JsValue> for Error<T> {
fn from(e: JsValue) -> Self {
Self::Binding(format!("{:?}", e))
}
}
// tauri.rs
type Response<T, E> = Result<T, crate::Error<E>>;
#[inline(always)]
pub async fn invoke<A: Serialize, R: DeserializeOwned, E: DeserializeOwned>(
cmd: &str,
args: &A,
) -> Response<R, E> {
let raw = inner::invoke(
cmd,
serde_wasm_bindgen::to_value(args).expect("serde binding error"),
)
.await;
match raw {
Ok(raw) => {
let res = serde_wasm_bindgen::from_value(raw);
match res {
Ok(res) => Ok(res),
Err(e) => Err(crate::Error::from(e)),
}
}
Err(e) => {
let error = serde_wasm_bindgen::from_value(e);
match error {
Ok(e) => Err(crate::Error::CustomError(e)),
Err(e) => Err(crate::Error::from(e)),
}
}
}
}
Agreed that tauri_sys::Error
should become public.
Is the scope of this change large? It seems like a pretty fundamental requirement.
Error handling is needed with commands like
invoke
, so I want to be able to handle the type on error as well (Users don't know when the error occurred). Therefore, I want to add enumResponseError
(I don't think this implementation is the best, so I will be fine when the same thing is implemented).Now, type of error is private, so it is a little helpful only that
tauri-sys::Error
become public (Maybe it is work in progress). I am happy that you will consider it.