wasm-bindgen: Replace Result with Result

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.

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 21
  • Comments: 16 (8 by maintainers)

Most upvoted comments

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<Error> for JsValue as would be required for ergonomic error handling.

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.

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.

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.

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.

This is somewhat similar to https://github.com/rustwasm/wasm-bindgen/issues/1017 and https://github.com/rustwasm/wasm-bindgen/issues/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

@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?