diff --git a/crates/napi/src/bindgen_runtime/js_values/stream/read.rs b/crates/napi/src/bindgen_runtime/js_values/stream/read.rs index 2b5fbb68..a8d39e45 100644 --- a/crates/napi/src/bindgen_runtime/js_values/stream/read.rs +++ b/crates/napi/src/bindgen_runtime/js_values/stream/read.rs @@ -497,6 +497,7 @@ impl futures_core::Stream for Reader { *chunk = Err(Error { status: Status::GenericFailure, reason: "".to_string(), + cause: None, maybe_raw: error_ref, maybe_env: cx.env.0, }); diff --git a/crates/napi/src/error.rs b/crates/napi/src/error.rs index 233e6c19..95914ac2 100644 --- a/crates/napi/src/error.rs +++ b/crates/napi/src/error.rs @@ -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 = std::result::Result>; @@ -22,6 +23,7 @@ pub type Result = std::result::Result>; pub struct Error = Status> { pub status: S, pub reason: String, + pub cause: Option>, // Convert raw `JsError` into Error pub(crate) maybe_raw: sys::napi_ref, pub(crate) maybe_env: sys::napi_env, @@ -47,6 +49,12 @@ impl> Drop for Error { } } +impl> Error { + pub fn set_cause(&mut self, cause: Error) { + self.cause = Some(Box::new(cause)); + } +} + impl> std::fmt::Debug for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( @@ -134,10 +142,17 @@ impl From> 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> = value + .coerce_to_object() + .and_then(|obj| obj.get_named_property::("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> for Error { Self { status: Status::GenericFailure, reason: "".to_string(), + cause: maybe_cause, maybe_raw: result, maybe_env, } @@ -174,6 +190,7 @@ impl> Error { Error { status, reason: reason.to_string(), + cause: None, maybe_raw: ptr::null_mut(), maybe_env: ptr::null_mut(), } @@ -183,6 +200,7 @@ impl> Error { Error { status, reason: "".to_owned(), + cause: None, maybe_raw: ptr::null_mut(), maybe_env: ptr::null_mut(), } @@ -200,6 +218,7 @@ impl + Clone> Error { 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 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 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 } diff --git a/crates/napi/src/threadsafe_function.rs b/crates/napi/src/threadsafe_function.rs index 1729bb29..689831a4 100644 --- a/crates/napi/src/threadsafe_function.rs +++ b/crates/napi/src/threadsafe_function.rs @@ -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, }) diff --git a/examples/napi/__tests__/__snapshots__/values.spec.ts.md b/examples/napi/__tests__/__snapshots__/values.spec.ts.md index 5284748a..c34986b8 100644 --- a/examples/napi/__tests__/__snapshots__/values.spec.ts.md +++ b/examples/napi/__tests__/__snapshots__/values.spec.ts.md @@ -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␊ diff --git a/examples/napi/__tests__/__snapshots__/values.spec.ts.snap b/examples/napi/__tests__/__snapshots__/values.spec.ts.snap index 86b50884..86d24628 100644 Binary files a/examples/napi/__tests__/__snapshots__/values.spec.ts.snap and b/examples/napi/__tests__/__snapshots__/values.spec.ts.snap differ diff --git a/examples/napi/__tests__/values.spec.ts b/examples/napi/__tests__/values.spec.ts index 092f89a6..fdeba47b 100644 --- a/examples/napi/__tests__/values.spec.ts +++ b/examples/napi/__tests__/values.spec.ts @@ -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`) } diff --git a/examples/napi/example.wasi-browser.js b/examples/napi/example.wasi-browser.js index 10913a71..34dfa368 100644 --- a/examples/napi/example.wasi-browser.js +++ b/examples/napi/example.wasi-browser.js @@ -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 diff --git a/examples/napi/example.wasi.cjs b/examples/napi/example.wasi.cjs index 93604b57..980680c4 100644 --- a/examples/napi/example.wasi.cjs +++ b/examples/napi/example.wasi.cjs @@ -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 diff --git a/examples/napi/index.cjs b/examples/napi/index.cjs index 30cb15b5..edbf086d 100644 --- a/examples/napi/index.cjs +++ b/examples/napi/index.cjs @@ -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 diff --git a/examples/napi/index.d.cts b/examples/napi/index.d.cts index 0ebf0011..4ab61bfe 100644 --- a/examples/napi/index.d.cts +++ b/examples/napi/index.d.cts @@ -947,6 +947,8 @@ export declare function throwAsyncError(): Promise 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 diff --git a/examples/napi/src/error.rs b/examples/napi/src/error.rs index 986fbe75..020535de 100644 --- a/examples/napi/src/error.rs +++ b/examples/napi/src/error.rs @@ -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");