Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace Result<T, JsValue> with Result<T, Error> #1742

Closed
Pauan opened this issue Aug 28, 2019 · 16 comments
Closed

Replace Result<T, JsValue> with Result<T, Error> #1742

Pauan opened this issue Aug 28, 2019 · 16 comments
Labels
breaking-change Tracking breaking changes for the next major version bump (if ever) web-sys Issues related to the `web-sys` crate

Comments

@Pauan
Copy link
Contributor

Pauan commented Aug 28, 2019

Motivation

As discussed in #1735, it's not idiomatic to throw JS values, like throw "foo", because that loses stack traces.

Instead it's highly recommended to always use throw new Error("foo"). In wasm-bindgen, this corresponds to Err(js_sys::Error::new("foo"))

Internally, wasm-bindgen itself follows this recommendation, however, because the wasm-bindgen (and js-sys and web-sys) APIs return Result<T, JsValue>, this encourages user functions to return Result<T, JsValue> as well, which makes it easy for users to return non-Error values:

fn foo() -> Result<(), JsValue> {
    call_some_wasm_bindgen_api()?;
    Err(JsValue::from("hi!"))
}

Proposed Solution

All wasm-bindgen APIs which return Result<T, JsValue> should instead return Result<T, Error>.

This includes wasm_bindgen(catch) (which should have a debug_assert to verify that it actually is an Error object)

This uses the static type system to make it impossible to accidentally throw non-Error types. It also is more "Rust-like", which makes the APIs easier to understand.

This is obviously a very large breaking change.

Alternatives

  • Do nothing. This is a lot less work, and isn't a breaking change, but it does mean that wasm-bindgen is encouraging bad behavior.

  • Keep Result<T, JsValue>, but put in runtime checks which verify that the Err is a js_sys::Error. This is also a lot less work, but it does have a small runtime cost, and it's surprising behavior for the user.

@alexcrichton
Copy link
Contributor

This is somewhat similar to #1017 and #1004 as well I think. I'd love to start immediately making progress on this if we can instead of delaying it to a batch of breaking changes though, that'd help us get our feet wet and experiment with with possible ideas before we set it all in stone.

One thing we might want to do to start out is to support returning custom error types, converting them with Display to a new Error(rust_error.to_string()) (basically). Next it might be prudent to go ahead and start warning about Err(JsValue::from("hi")) in some capacity, or maybe even moving Error into wasm-bindgen the crate itself to make it more easily available

@Pauan
Copy link
Contributor Author

Pauan commented Sep 4, 2019

One thing we might want to do to start out is to support returning custom error types, converting them with Display to a new Error(rust_error.to_string()) (basically).

That will have bad stack traces though. Because of the way errors work in JS, you basically always want to create the Error object at the specific place where the error occurred.

However, a custom error type which contains a js_sys::Error is probably a good idea. We can have macros to make it easy to generate them.

Stdweb already does this extensively. It has attributes and macros to generate the error types, and it uses them in the APIs.


For web-sys, we should add in a From<web_sys::DomException> for js_sys::Error impl (exact implementation to be decided), and all of the web-sys APIs would error with DomException instead of JsValue.

Ideally we would want more fine-grained errors for web-sys, like this:

#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(
        extends = DomException,
        extends = Error,
        instanceof = InvalidCharacterError::instanceof
    )]
    type InvalidCharacterError;
}

impl InvalidCharacterError {
    fn instanceof(val: &JsValue) -> bool {
        if let Some(exception) = val.dyn_ref::<DomException>() {
            exception.name() == "InvalidCharacterError"

        } else {
            false
        }
    }
}

But the problem is that WebIDL doesn't specify which errors each API throws, so we would need to manually add those in. And it may not even be worth it, since DomException is probably good enough.

@Pauan
Copy link
Contributor Author

Pauan commented Sep 4, 2019

Oh, I think I understand what you mean about custom error types. You mean something like this:

impl<E> From<E> for js_sys::Error where E: std::error::Error {
    #[inline]
    fn from(value: E) -> js_sys::Error {
        js_sys::Error::new(&value.to_string())
    }
}

And then it would be possible to use ? inside of a function and it would auto-convert the custom error into a js_sys::Error.

I think that's a good idea. The stack traces might not always be 100% ideal, but they'll be pretty good.

@alexcrichton
Copy link
Contributor

Yes I don't think that capturing stack traces with the higest of fidelity is that important. Additionally while changing all of web-sys might be nice I think it's fine to leave that for later. For now I'd just like to explore avenues to unlock more functionality today rather than waiting on a bunch of changes to happen "eventually"

@cormacrelf
Copy link
Contributor

Not sure if it's just me, but it doesn't seem like it's currently possible to construct a js_sys::Error without the glue code catching the error you're trying to create. (Bit weird, catch shouldn't catch an Error when it's just a return value from a new Error() call.) When I return Err(Error::new("...")), I just get this in the console:

wasm-bindgen: imported JS function that was not marked as `catch` threw an error: my custom error message here

Stack:
__wbg_new_2e9dc2ca6bd84218@webpack-internal:///../pkg/citeproc_wasm.js:680:17
__wbg_new_2e9dc2ca6bd84218@http://localhost:8081/index.js:104:75
js_sys::Error::new::h2f12b4de882df30f@http://localhost:8081/b0a7cc0755979b50ad35.module.wasm:wasm-function[11137]:0x2075a3
my_module::...

@alexcrichton
Copy link
Contributor

@cormacrelf I think you may be running into a separate issue that's too eagerly attempting to print out that a function threw an error, mind opening up a new issue for that?

@That3Percent
Copy link

I like the idea of having some sort of impl<E> From<E> for js_sys::Error where E: std::error::Error {

Right now it's a papercut to try to use an error crate like Anyhow. Because of the orphan rule, a user crate can't implement From for JsValue as would be required for ergonomic error handling.

@tonyhb
Copy link

tonyhb commented Jun 14, 2020

Adding Result<T, Error> is going to be much nicer for interop!

Currently working around the lack of Result<T, Error> by defining multiple impl Foo blocks with different targets. It's annoying but it works.

Is there a better way? Been doing rust for all of 2 days.

#[cfg(target_arch = "wasm32")]
macro_rules! jserr {
    ($expression:expr) => {
        match $expression {
            Ok(a) => Ok(a),
            Err(e) => {
                return Err(JsValue::from(format!("{}", e)));
            }
        }
    };
}

// wasm only.  wasm doesn't respect
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
impl Project {
    #[wasm_bindgen]
    pub fn new(zipdata: Vec<u8>) -> Result<Project, JsValue> {
        jserr!(Project::from_zipdata(zipdata))
    }
}

// Non-wasm functions.  Crappy overloading.
#[cfg(not(target_arch = "wasm32"))]
impl Project {
    pub fn new(zipdata: Vec<u8>) -> Result<Project, anyhow::Error> {
        Project::from_zipdata(zipdata)
    }
}

// Shared, non-accessible wasm functions.
impl Project {
    fn from_zipdata(zipdata: Vec<u8>) -> Result<Project, anyhow::Error> {
        let mut archive = zip::ZipArchive::new(Cursor::new(zipdata))?;
        let file = archive.by_name("foo.json")?;
        let mut ds = serde_json::Deserializer::from_reader(file);
        Project::deserialize(&mut ds)?
    }
}

Another issue here is that wasm_bindgen doesn't respect #[cfg(not(target_arch = "wasm32"))] - if you have methods that return Result with boxed errors explicitly not built in the wasm target, the compiler still complains that the trait converting into wasm ABI isn't implemented. This is why there are multiple impls.

@austinjones
Copy link

I'm working on a fairly large WASM project, and error handling is rough.

Whenever I call a 'rust' program, I have to convert errors into JsValues.
Whenever I call web_sys/js_sys from a rust program with proper error handling (using anyhow), I have to manually convert the JsValue into an anyhow error.

This issue would make error handling so much more ergonomic.

@tmcguire
Copy link

As a side note, it would be nice to update https://rustwasm.github.io/wasm-bindgen/reference/types/result.html with the recommendation to use js_sys::Error, and maybe with an example.

@renatoathaydes
Copy link

renatoathaydes commented Aug 10, 2021

Just wanted to mention that this is also my biggest problem working with web-sys. All my JS-calling Rust functions currently return Result<T, JsValue> because doing otherwise means lots of boilerplate... but that of course means the boundary between Rust and JS always requires type conversions, which is itself tricky as you never know if the Error object is a JS Error, or a String or any other JS type.

If anyone has a band-aid solution for this that works right now, please let me (and the others suffering for this) know!

OTOH, one of the problems I have is that Error cannot be sent in postMessage in all browsers, so having all errors be enforced to be JS Error would make things actually harder... for example, we have to do this in some places to avoid having trouble getting through postMessage (and to not show only the SyntaxError: JSON.parse: unexpected... message which alone would not be useful to someone debugging this):

fn json_error(js_error: JsValue) -> JsValue {
    let err: Error = js_error.into();
    JsValue::from(format!(
        "payload is not valid JSON: {:?}", err.to_string()))
}

Not sure if there's a better way (what if js_error is not really Error?), but this will later go into one of the Result<T, JsonValue> and be sent to the UI window via postMessage and we need the extra context to the error message...

@cataggar
Copy link

I saw yesterday that V8 release 9.5 added support for WebAssembly Exception Handling.
https://v8.dev/blog/v8-release-95#exception-handling

Is there a way for wasm-bindgen to take advantage of that now?

@cormacrelf
Copy link
Contributor

cormacrelf commented Dec 1, 2021

Latest update is that @alexcrichton and I have been working on #2710, and we can hopefully merge it pretty soon. It offers a pretty broad-spectrum improvement. Aside from the stack space issue, it extends the range of Result::Err types to T: Into<JsValue>. You can put anything in there. If you do return an Err, the value in it gets converted to a JsValue and returned for the JavaScript glue to picks up and throw for the first time.

It also ships a very simple JsError type, which is a bit like the wasm_bindgen::JsValue analogue to js_sys::Object, but for errors. It only has three features:

  1. JsError::new(&str) constructs a new Error("..."), but does not throw it.
  2. It implements Into<JsValue> and can therefore be used in Result return values
  3. it implements conversion from E: std::error::Error so if you already have error types or you use an error handling library, you can easily get a new Error("...") with their error message, using ? in a fn() -> Result<_, JsError> function. (This was proposed above: Replace Result<T, JsValue> with Result<T, Error> #1742 (comment))
#[wasm_bindgen]
pub fn throw_an_error() -> Result<i32, JsError> {
    subroutine()?;
    Err(JsError::new("error message"))
}

Of course you do not have to use JsError, and one upgrade would be to import a custom Error subclass from JavaScript and return one of those instead. This is what I've been doing manually in my project (with corresponding ugly .unwrap() from JavaScript) and it can be worthwhile if you need your library consumers to know which errors are yours.

I think that addresses most of what we need, and also does not constitute a breaking change. You can still throw arbitrary JsValues. Most of the discussion on the PR has been about the tricky implementation of the delayed throwing, so anyone who had feedback about how the ergonomic improvements work should pipe up.

@alexcrichton
Copy link
Contributor

I'm going to close this having largely been implemented in #2710, thanks @cormacrelf for all your work implementing this! As mentioned in #2710 (comment) some docs probably need to get updated, but I think it's fine to track that separately.

@vdods
Copy link

vdods commented Jun 19, 2022

I'm also running into some headaches because I use anyhow::Error (and am confused by why it doesn't impl std::error::Error).

Would it make sense to add an impl From<anyhow::Error> for JsError which is gated on an "anyhow" feature in wasm-bindgen?

@cryptoquick
Copy link

Definitely cut myself on this one. Solution was:

fn_that_returns_an_anyhow_result().map_err(|err| JsError::new(&err.to_string()))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
breaking-change Tracking breaking changes for the next major version bump (if ever) web-sys Issues related to the `web-sys` crate
Projects
None yet
Development

No branches or pull requests