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

Throwing custom error subclass object #1981

Open
victorteokw opened this issue Feb 25, 2024 · 6 comments
Open

Throwing custom error subclass object #1981

victorteokw opened this issue Feb 25, 2024 · 6 comments

Comments

@victorteokw
Copy link
Contributor

Hi @Brooooooklyn,

Seems like the maybe_raw field of the Error struct, represents the error object in Js. How can I define and throw an Error of a custom Error subclass? Sometimes users want to throw something like https://github.com/jshttp/http-errors,
new HTTPError("MyBadRequest", 400).

I understand that you are busy, can you give me some instructions on how to implement this? I can implement and fire a PR.

@lbarthon
Copy link
Contributor

Isn't this a behaviour that is working? If you throw an error subclass, and it implements toString, napi-rs will call napi_coerce_to_string and create a GenericFailure error with it as a reason.

Would you want a way to parse the custom error type from within Rust? Do you have a code snippet of how you'd expect it to work?

@victorteokw
Copy link
Contributor Author

Hi @lbarthon,

The error implementation uses a temporary Rust value which will be converted to a Js value at correct time. If a custom error subclass instance is passed from Js, the Rust error cannot retrieve all of its fields. There is no way to create an error of an Error subclass in Rust. Developers from the Js side use instanceof to determine the error type. However, our current implementation can only change an error code which is the title of the error. We cannot assign custom fields to the error nor specify a custom error class.

The pyo3 Rust Python binding package allows to throw an error of custom subclass defined in either Rust or Python. And the PyObject equivalent of the error class can be retrieved.

See this https://github.com/jshttp/http-errors, for node developers, it makes sense to define an error subclass with custom fields.

To support this, several things could be done:

  1. Define a trait maybe named JsErrorRepresentable and it can be converted from and to JsError
  2. Use a generic parameter E instead of current Error
  3. User define a custom class which implements the JsErrorRepresentable that we defined previously, and on passing from the Js side, user defines how to retrieve field values, on creation from Rust, user specify values, on converting to Js, use the callback that user provided to convert it to js value

I can provide more information if it's unclear.

Thanks

@kraenhansen
Copy link

kraenhansen commented Feb 29, 2024

I too want to throw instances of specific subclasses of (the JS global) Error, since the calling code is performing instanceof comparisons to detect the type of failure.

One idea I had was to expose a function (I can simply use a constructor in my case) from Rust to be called by JS to inject the constructors for these custom errors, enabling Rust to create instances of them. But .. to be honest, I'm struggling with declaring the type of the function taking these Error constructors 🤔

Also, I noticed #1205 while searching the repo for other issues related to this.

@victorteokw
Copy link
Contributor Author

This example https://pyo3.rs/v0.20.3/exception demonstrates how to import and expose platform errors in Rust code. This is how pyo3 implemented for Python.

@kraenhansen
Copy link

kraenhansen commented Mar 5, 2024

I've made a bit of progress on a workaround for this as I noticed the impl From<JsUnknown> for Error:

impl From<JsUnknown> for Error {
fn from(value: JsUnknown) -> Self {
let mut result = std::ptr::null_mut();
let status = unsafe { sys::napi_create_reference(value.0.env, value.0.value, 1, &mut result) };
if status != sys::Status::napi_ok {
return Error::new(
Status::from(status),
"Create Error reference failed".to_owned(),
);
}
let maybe_error_message = value
.coerce_to_string()
.and_then(|a| a.into_utf8().and_then(|a| a.into_owned()));
if let Ok(error_message) = maybe_error_message {
return Self {
status: Status::GenericFailure,
reason: error_message,
maybe_raw: result,
};
}
Self {
status: Status::GenericFailure,
reason: "".to_string(),
maybe_raw: result,
}
}
}

Inject and fail in a single call

More specifically, it's possible to create an instance of a custom error and construct a napi::Error from it:

use napi::{bindgen_prelude::*, JsUndefined};

#[macro_use]
extern crate napi_derive;

#[napi]
pub fn inject_and_fail(error_constructor: JsFunction) -> napi::Result<JsUndefined> {
  let args: Vec<napi::JsUnknown> = vec![];
  let error = error_constructor.new_instance(&args).unwrap();
  Err(napi::Error::from(error.into_unknown()))
}

Inject and fail across calls

But ... for my use-case I want to inject the custom error from JS and store it for later use by another function.
I've tried storing the JsFunction in a thread_local RefCell, but this panics at the unwrap of the Result returned from new_instance as this errors with Error { status: InvalidArg, reason: "", maybe_raw: 0x0 } (and I have a hard time figuring out the root cause of that TBO).

use napi::{bindgen_prelude::*, JsUndefined};

#[macro_use]
extern crate napi_derive;

thread_local! {
  static ERROR_CONSTRUCTOR: RefCell<JsFunction> = panic!("Error constructor accessed before injection");
}

#[napi]
pub fn inject(error_constructor: napi::JsFunction) {
  ERROR_CONSTRUCTOR.set(error_constructor);
}

#[napi]
pub fn fail() -> napi::Result<JsUndefined> {
  let args: Vec<napi::JsUnknown> = vec![];
  let error = ERROR_CONSTRUCTOR
    .with(|error_constructor| error_constructor.borrow().new_instance(&args).unwrap());
  Err(napi::Error::from(error.into_unknown()))
}

Perhaps someone more knowledgeable can hint to the underlying issue?

@victorteokw
Copy link
Contributor Author

Hi @kraenhansen,

To retrieve the data in the error in Rust, an env is necessary. When throwing an error from the Rust side, the struct catches the values, currently a code which is a title, and a message. When the conversion is performed, the title and the message are retrieved and a js object which represents the error is built.

There are several scenarios that a cross error is used:

  1. throw in Rust env is not always present especially in async callbacks. We must declare a rust struct to hold the data fields.
  2. throw in JS As long as the error is exposed. No matter on which side the error is declared.
  3. read in Rust When reading a rust defined error, just read the struct values. When reading a JS defined error, it was converted to the rust struct previously.
  4. read in JS As long as the error is exposed. No matter on which side the error is declared.

It's hard to implement unless the current error strategy is fully updated. The current API that returns an Err variant of napi::Error, might need to return some E which implements some newly defined trait.

Hi @Brooooooklyn, could you give us some ideas on doing this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants