diff --git a/CHANGELOG.md b/CHANGELOG.md index 42d677862..25577ca63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Features * [FEATURE] Distinct `ApplyTransactionAfterSubmitFailed` error variant with one apply retry, so callers know a submitted tx landed on-chain and should not be re-submitted ([#2059](https://github.com/0xMiden/miden-client/pull/2059)). +* [FEATURE][web] Stable `errorCode` string property on thrown JS errors for variant dispatch without message-string parsing ([#2060](https://github.com/0xMiden/miden-client/pull/2060)). * [FEATURE][web] Added `"custom"` operation to `preview()` so users can dry-run any pre-built `TransactionRequest`, not just send/mint/consume/swap ([#2052](https://github.com/0xMiden/miden-client/pull/2052)). ## 0.14.3 (2026-04-16) diff --git a/crates/web-client/src/lib.rs b/crates/web-client/src/lib.rs index 5204760c8..a5f941bd8 100644 --- a/crates/web-client/src/lib.rs +++ b/crates/web-client/src/lib.rs @@ -349,12 +349,21 @@ where } let help = hint_from_error(&err); + let code = error_code_from_error(&err); let js_error: JsValue = JsError::new(&error_string).into(); if let Some(help) = help { let _ = Reflect::set(&js_error, &JsValue::from_str("help"), &JsValue::from_str(&help)); } + // Stable, machine-readable code for consumers that need to react + // differently based on the specific ClientError variant. The string + // text is load-bearing — see `error_code_from_client_error` for the + // list of codes and their contract. + if let Some(code) = code { + let _ = Reflect::set(&js_error, &JsValue::from_str("errorCode"), &JsValue::from_str(code)); + } + js_error } @@ -365,3 +374,32 @@ fn hint_from_error(err: &(dyn Error + 'static)) -> Option { err.source().and_then(hint_from_error) } + +/// Walks the error chain looking for a [`ClientError`] and returns a +/// stable, machine-readable code for it. Used by [`js_error_with_context`] +/// to attach an `errorCode` string to the JS Error — consumers can +/// pattern-match on it to trigger failure-mode-specific handling +/// (e.g. treat a submitted-but-not-applied tx as Completed rather than +/// Failed). Codes are the variant names; the contract is that they +/// don't change across releases. +fn error_code_from_error(err: &(dyn Error + 'static)) -> Option<&'static str> { + if let Some(client_error) = err.downcast_ref::() { + return error_code_from_client_error(client_error); + } + err.source().and_then(error_code_from_error) +} + +fn error_code_from_client_error(err: &ClientError) -> Option<&'static str> { + // Only include variants consumers are known to dispatch on. Others + // can be added as new callers need them — the stability contract is + // "code string never changes once added." + match err { + ClientError::ApplyTransactionAfterSubmitFailed { .. } => { + Some("ApplyTransactionAfterSubmitFailed") + }, + ClientError::AccountLocked(_) => Some("AccountLocked"), + ClientError::NoteNotFoundOnChain(_) => Some("NoteNotFoundOnChain"), + ClientError::RpcError(_) => Some("RpcError"), + _ => None, + } +}