feat(napi): add Error.cause support to napi::Error (#2829)

Co-authored-by: LongYinan <lynweklm@gmail.com>
This commit is contained in:
2025-08-05 13:53:36 +09:00 committed by GitHub
parent 04aacf4abc
commit 7f5013eee3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 52 additions and 0 deletions

View File

@ -497,6 +497,7 @@ impl<T: FromNapiValue + 'static> futures_core::Stream for Reader<T> {
*chunk = Err(Error {
status: Status::GenericFailure,
reason: "".to_string(),
cause: None,
maybe_raw: error_ref,
maybe_env: cx.env.0,
});

View File

@ -12,6 +12,7 @@ use serde::{de, ser};
#[cfg(feature = "serde-json")]
use serde_json::Error as SerdeJSONError;
use crate::bindgen_runtime::JsObjectValue;
use crate::{bindgen_runtime::ToNapiValue, check_status, sys, Env, JsValue, Status, Unknown};
pub type Result<T, S = Status> = std::result::Result<T, Error<S>>;
@ -22,6 +23,7 @@ pub type Result<T, S = Status> = std::result::Result<T, Error<S>>;
pub struct Error<S: AsRef<str> = Status> {
pub status: S,
pub reason: String,
pub cause: Option<Box<Error>>,
// Convert raw `JsError` into Error
pub(crate) maybe_raw: sys::napi_ref,
pub(crate) maybe_env: sys::napi_env,
@ -47,6 +49,12 @@ impl<S: AsRef<str>> Drop for Error<S> {
}
}
impl<S: AsRef<str>> Error<S> {
pub fn set_cause(&mut self, cause: Error) {
self.cause = Some(Box::new(cause));
}
}
impl<S: AsRef<str>> std::fmt::Debug for Error<S> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
@ -134,10 +142,17 @@ impl From<Unknown<'_>> for Error {
let maybe_error_message = value
.coerce_to_string()
.and_then(|a| a.into_utf8().and_then(|a| a.into_owned()));
let maybe_cause: Option<Box<Error>> = value
.coerce_to_object()
.and_then(|obj| obj.get_named_property::<Unknown>("cause"))
.map(|cause| Box::new(cause.into()))
.ok();
if let Ok(error_message) = maybe_error_message {
return Self {
status: Status::GenericFailure,
reason: error_message,
cause: maybe_cause,
maybe_raw: result,
maybe_env,
};
@ -146,6 +161,7 @@ impl From<Unknown<'_>> for Error {
Self {
status: Status::GenericFailure,
reason: "".to_string(),
cause: maybe_cause,
maybe_raw: result,
maybe_env,
}
@ -174,6 +190,7 @@ impl<S: AsRef<str>> Error<S> {
Error {
status,
reason: reason.to_string(),
cause: None,
maybe_raw: ptr::null_mut(),
maybe_env: ptr::null_mut(),
}
@ -183,6 +200,7 @@ impl<S: AsRef<str>> Error<S> {
Error {
status,
reason: "".to_owned(),
cause: None,
maybe_raw: ptr::null_mut(),
maybe_env: ptr::null_mut(),
}
@ -200,6 +218,7 @@ impl<S: AsRef<str> + Clone> Error<S> {
Ok(Self {
status: self.status.clone(),
reason: self.reason.to_string(),
cause: None,
maybe_raw: self.maybe_raw,
maybe_env: self.maybe_env,
})
@ -211,6 +230,7 @@ impl Error {
Error {
status: Status::GenericFailure,
reason: reason.into(),
cause: None,
maybe_raw: ptr::null_mut(),
maybe_env: ptr::null_mut(),
}
@ -222,6 +242,7 @@ impl From<std::ffi::NulError> for Error {
Error {
status: Status::GenericFailure,
reason: format!("{error}"),
cause: None,
maybe_raw: ptr::null_mut(),
maybe_env: ptr::null_mut(),
}
@ -233,6 +254,7 @@ impl From<std::io::Error> for Error {
Error {
status: Status::GenericFailure,
reason: format!("{error}"),
cause: None,
maybe_raw: ptr::null_mut(),
maybe_env: ptr::null_mut(),
}
@ -382,6 +404,16 @@ macro_rules! impl_object_methods {
debug_assert!(create_reason_status == sys::Status::napi_ok);
let create_error_status = unsafe { $kind(env, error_code, reason_string, &mut js_error) };
debug_assert!(create_error_status == sys::Status::napi_ok);
if let Some(cause_error) = self.0.cause.take() {
let cause = ToNapiValue::to_napi_value(env, *cause_error)
.expect("Convert cause Error to napi_value should never error");
let set_cause_status =
unsafe { sys::napi_set_named_property(env, js_error, c"cause".as_ptr().cast(), cause) };
debug_assert!(
set_cause_status == sys::Status::napi_ok,
"Set cause property failed"
);
}
js_error
}

View File

@ -755,6 +755,7 @@ unsafe extern "C" fn call_js_cb<
Err(Error {
maybe_raw: error_reference,
maybe_env: raw_env,
cause: None,
status: Status::from(raw_status),
reason,
})

View File

@ -986,6 +986,8 @@ Generated by [AVA](https://avajs.dev).
export declare function throwError(): void␊
export declare function throwErrorWithCause(): void␊
export declare function throwSyntaxError(error: string, code?: string | undefined | null): void␊
export declare function toJsObj(): object␊

View File

@ -58,6 +58,7 @@ import {
mapOption,
readFile,
throwError,
throwErrorWithCause,
jsErrorCallback,
customStatusCode,
panic,
@ -872,6 +873,9 @@ test('Option', (t) => {
test('Result', (t) => {
t.throws(() => throwError(), void 0, 'Manual Error')
const errorWithCause = t.throws(() => throwErrorWithCause())
t.is(errorWithCause?.message, 'Manual Error')
t.is((errorWithCause?.cause as Error)?.message, 'Inner Error')
if (!process.env.SKIP_UNWIND_TEST) {
t.throws(() => panic(), void 0, `Don't panic`)
}

View File

@ -343,6 +343,7 @@ export const threadsafeFunctionThrowError = __napiModule.exports.threadsafeFunct
export const threadsafeFunctionThrowErrorWithStatus = __napiModule.exports.threadsafeFunctionThrowErrorWithStatus
export const throwAsyncError = __napiModule.exports.throwAsyncError
export const throwError = __napiModule.exports.throwError
export const throwErrorWithCause = __napiModule.exports.throwErrorWithCause
export const throwSyntaxError = __napiModule.exports.throwSyntaxError
export const toJsObj = __napiModule.exports.toJsObj
export const tsfnAsyncCall = __napiModule.exports.tsfnAsyncCall

View File

@ -388,6 +388,7 @@ module.exports.threadsafeFunctionThrowError = __napiModule.exports.threadsafeFun
module.exports.threadsafeFunctionThrowErrorWithStatus = __napiModule.exports.threadsafeFunctionThrowErrorWithStatus
module.exports.throwAsyncError = __napiModule.exports.throwAsyncError
module.exports.throwError = __napiModule.exports.throwError
module.exports.throwErrorWithCause = __napiModule.exports.throwErrorWithCause
module.exports.throwSyntaxError = __napiModule.exports.throwSyntaxError
module.exports.toJsObj = __napiModule.exports.toJsObj
module.exports.tsfnAsyncCall = __napiModule.exports.tsfnAsyncCall

View File

@ -673,6 +673,7 @@ module.exports.threadsafeFunctionThrowError = nativeBinding.threadsafeFunctionTh
module.exports.threadsafeFunctionThrowErrorWithStatus = nativeBinding.threadsafeFunctionThrowErrorWithStatus
module.exports.throwAsyncError = nativeBinding.throwAsyncError
module.exports.throwError = nativeBinding.throwError
module.exports.throwErrorWithCause = nativeBinding.throwErrorWithCause
module.exports.throwSyntaxError = nativeBinding.throwSyntaxError
module.exports.toJsObj = nativeBinding.toJsObj
module.exports.tsfnAsyncCall = nativeBinding.tsfnAsyncCall

View File

@ -947,6 +947,8 @@ export declare function throwAsyncError(): Promise<void>
export declare function throwError(): void
export declare function throwErrorWithCause(): void
export declare function throwSyntaxError(error: string, code?: string | undefined | null): void
export declare function toJsObj(): object

View File

@ -5,6 +5,13 @@ pub fn throw_error() -> Result<()> {
Err(Error::new(Status::InvalidArg, "Manual Error".to_owned()))
}
#[napi]
pub fn throw_error_with_cause() -> Result<()> {
let mut err = Error::new(Status::GenericFailure, "Manual Error".to_owned());
err.set_cause(Error::new(Status::InvalidArg, "Inner Error".to_owned()));
Err(err)
}
#[napi(catch_unwind)]
pub fn panic() {
panic!("Don't panic");