Error Macros
Dash Platform defines hundreds of consensus errors. Each one needs a WASM binding so JavaScript code can inspect error codes, read messages, and serialize errors for transport. Writing a wrapper struct for every single error by hand would be tedious, error-prone, and a maintenance burden.
This chapter covers the generic_consensus_error! macro that generates WASM bindings
automatically, the paste! crate that makes it work, the manual binding pattern for
errors that need custom methods, and how the two approaches coexist.
The Problem
Consider Platform's error hierarchy. At the top level:
ConsensusError
BasicError (dozens of variants)
StateError (dozens of variants)
SignatureError (handful of variants)
FeeError (one variant)
Each variant wraps a specific error struct -- TransitionNoInputsError,
DocumentNotFoundError, MasternodeNotFoundError, and so on. There are well over a
hundred of these.
Every one needs a JavaScript class with:
- A
getCode()method returning the numeric error code. - A
messagegetter returning the human-readable error string. - A
serialize()method for wire encoding. - A
From<&RustType>implementation for conversion.
Writing all of this for each error would mean hundreds of nearly identical files.
The generic_consensus_error! Macro
The macro lives in packages/wasm-dpp/src/errors/generic_consensus_error.rs:
#![allow(unused)] fn main() { #[macro_export] macro_rules! generic_consensus_error { ($error_type:ident, $error_instance:expr) => {{ use { dpp::{ consensus::{codes::ErrorWithCode, ConsensusError}, serialization::PlatformSerializableWithPlatformVersion, version::PlatformVersion, }, paste::paste, wasm_bindgen::prelude::wasm_bindgen, $crate::buffer::Buffer, }; paste! { #[derive(Debug)] #[wasm_bindgen(js_name=$error_type)] pub struct [<$error_type Wasm>] { inner: $error_type } impl From<&$error_type> for [<$error_type Wasm>] { fn from(e: &$error_type) -> Self { Self { inner: e.clone() } } } #[wasm_bindgen(js_class=$error_type)] impl [<$error_type Wasm>] { #[wasm_bindgen(js_name=getCode)] pub fn get_code(&self) -> u32 { ConsensusError::from(self.inner.clone()).code() } #[wasm_bindgen(getter)] pub fn message(&self) -> String { self.inner.to_string() } pub fn serialize(&self) -> Result<Buffer, JsError> { let bytes = ConsensusError::from(self.inner.clone()) .serialize_to_bytes_with_platform_version( PlatformVersion::first(), ) .map_err(JsError::from)?; Ok(Buffer::from_bytes(bytes.as_slice())) } } [<$error_type Wasm>]::from($error_instance) } }}; } }
What It Generates
For a call like generic_consensus_error!(MasternodeNotFoundError, e), the macro
generates:
- A wrapper struct:
MasternodeNotFoundErrorWasmwithinner: MasternodeNotFoundError - A From impl:
From<&MasternodeNotFoundError> for MasternodeNotFoundErrorWasm - Three methods:
get_code()-- returns the numeric error codemessage-- returns theDisplaystringserialize()-- encodes the error to bytes
- An instantiation: Creates a
MasternodeNotFoundErrorWasmfrom the error reference
The paste! Crate
The magic of [<$error_type Wasm>] comes from the paste crate. Standard Rust
macros cannot concatenate identifiers -- you cannot write $error_type ## Wasm to
create a new identifier. The paste! macro provides this:
#![allow(unused)] fn main() { paste! { pub struct [<$error_type Wasm>] { ... } // ^^^^^^^^^^^^^^^^^ // pastes to: MasternodeNotFoundErrorWasm } }
Inside paste! { ... }, [<token1 token2>] concatenates tokens into a single
identifier. This is what allows the macro to generate both the JavaScript name
(MasternodeNotFoundError via js_name) and the Rust struct name
(MasternodeNotFoundErrorWasm via paste).
How It Is Used
The macro is used inline within the from_consensus_error_ref function and its
helpers in packages/wasm-dpp/src/errors/consensus/consensus_error.rs. Here is
a representative excerpt:
#![allow(unused)] fn main() { pub fn from_state_error(state_error: &StateError) -> JsValue { match state_error { // Manual wrappers (have custom methods) StateError::DocumentAlreadyPresentError(e) => { DocumentAlreadyPresentErrorWasm::from(e).into() } StateError::DocumentNotFoundError(e) => { DocumentNotFoundErrorWasm::from(e).into() } // Macro-generated wrappers (standard interface only) StateError::MasternodeNotFoundError(e) => { generic_consensus_error!(MasternodeNotFoundError, e).into() } StateError::DocumentContestCurrentlyLockedError(e) => { generic_consensus_error!( DocumentContestCurrentlyLockedError, e ).into() } StateError::TokenIsPausedError(e) => { generic_consensus_error!(TokenIsPausedError, e).into() } // ... dozens more } } }
The pattern is clear: use the macro for errors that need only getCode(), message,
and serialize(). Use manual wrappers for errors that need custom accessors.
Manual Wrappers: When You Need More
Some errors expose domain-specific data that JavaScript code needs to access. For
these, you write a full wrapper by hand. Here is DataContractMaxDepthExceedError
from packages/wasm-dpp/src/errors/consensus/basic/data_contract/:
#![allow(unused)] fn main() { #[wasm_bindgen(js_name=DataContractMaxDepthExceedError)] pub struct DataContractMaxDepthExceedErrorWasm { inner: DataContractMaxDepthExceedError, } impl From<&DataContractMaxDepthExceedError> for DataContractMaxDepthExceedErrorWasm { fn from(e: &DataContractMaxDepthExceedError) -> Self { Self { inner: e.clone() } } } #[wasm_bindgen(js_class=DataContractMaxDepthError)] impl DataContractMaxDepthExceedErrorWasm { #[wasm_bindgen(js_name=getMaxDepth)] pub fn get_max_depth(&self) -> usize { self.inner.max_depth() } #[wasm_bindgen(js_name=getCode)] pub fn get_code(&self) -> u32 { ConsensusError::from(self.inner.clone()).code() } #[wasm_bindgen(getter)] pub fn message(&self) -> String { self.inner.to_string() } } }
The custom get_max_depth() method lets JavaScript inspect the specific limit that
was exceeded. The macro cannot generate these domain-specific accessors -- it only
knows about the three standard methods.
The from_dpp_err Pattern
At the top level, Rust's ProtocolError is converted to a JsValue through
from_dpp_err in packages/wasm-dpp/src/errors/from.rs:
#![allow(unused)] fn main() { pub fn from_dpp_err(pe: ProtocolError) -> JsValue { match pe { ProtocolError::ConsensusError(consensus_error) => { from_consensus_error(*consensus_error) } ProtocolError::DataContractError(e) => { from_data_contract_to_js_error(e) } ProtocolError::Document(e) => { from_document_to_js_error(*e) } ProtocolError::DataContractNotPresentError(err) => { DataContractNotPresentNotConsensusErrorWasm::new( err.data_contract_id() ).into() } ProtocolError::ValueError(value_error) => { PlatformValueErrorWasm::from(value_error).into() } _ => JsValue::from_str( &format!("Error conversion not implemented: {pe:#}") ), } } }
This is the entry point for error conversion. It dispatches to the appropriate conversion function based on the error variant. The fallback case converts unhandled errors to a string -- not ideal, but it ensures no errors are silently swallowed.
The Consensus Error Dispatch
The from_consensus_error_ref function dispatches across the entire consensus
error hierarchy:
#![allow(unused)] fn main() { pub fn from_consensus_error_ref(e: &DPPConsensusError) -> JsValue { match e { DPPConsensusError::FeeError(e) => match e { FeeError::BalanceIsNotEnoughError(e) => BalanceIsNotEnoughErrorWasm::from(e).into(), }, DPPConsensusError::SignatureError(e) => from_signature_error(e), DPPConsensusError::StateError(state_error) => from_state_error(state_error), DPPConsensusError::BasicError(basic_error) => from_basic_error(basic_error), DPPConsensusError::DefaultError => JsError::new("DefaultError").into(), } } }
Each sub-function (from_state_error, from_basic_error, from_signature_error)
handles its category, using either manual wrappers or the macro as appropriate.
Adding a New WASM Error Binding
When a new consensus error is added to rs-dpp, you need to add its WASM binding.
Here is the checklist:
If the error only needs getCode(), message, and serialize():
- Import the error type in
consensus_error.rs. - Add a match arm using the macro:
#![allow(unused)] fn main() { StateError::YourNewError(e) => { generic_consensus_error!(YourNewError, e).into() } }
That is it. The macro handles everything else.
If the error needs custom accessors:
- Create a new file in the appropriate subdirectory under
packages/wasm-dpp/src/errors/consensus/. - Define the wrapper struct,
Fromimpl, and methods following the manual pattern. - Add the wrapper to the
mod.rsfile. - Import it in
consensus_error.rs. - Add a match arm using the manual wrapper:
#![allow(unused)] fn main() { StateError::YourNewError(e) => { YourNewErrorWasm::from(e).into() } }
Rules
Do:
- Use
generic_consensus_error!for errors that need only the standard three methods. - Use manual wrappers when JavaScript needs to access error-specific fields.
- Follow the existing file organization: one file per manual error, grouped by category.
- Always provide
getCode(),message, andserialize()-- these are the standard interface. - Test that new errors convert correctly in both directions.
Don't:
- Write manual wrappers when the macro would suffice -- it just creates maintenance debt.
- Forget to add the match arm in
consensus_error.rs-- unhandled errors will fall through to theDefaultErrorcase or anunreachable_patternsguard. - Use
js_namevalues that differ from the Rust error type name -- JavaScript developers should see the same name they find in documentation. - Skip the
serialize()method -- it is needed for error transport across process boundaries. - Modify
generic_consensus_error!without understanding that every call site will be affected -- it generates code in every match arm that uses it.