Handle errors
Catch, match, and recover from SDK errors in your application.
All errors thrown by @zama-fhe/sdk and @zama-fhe/react-sdk extend ZamaError and carry a .code string for programmatic matching. This guide covers how to catch them, route them to user-friendly messages, and troubleshoot common problems.
Steps
1. Understand the error hierarchy
Every SDK error is an instance of ZamaError, which extends the native Error class. Each subclass has a unique .code property:
SigningRejectedError
SIGNING_REJECTED
User rejected the wallet signature
SigningFailedError
SIGNING_FAILED
Wallet signature failed (connectivity or firmware issue)
EncryptionFailedError
ENCRYPTION_FAILED
FHE encryption failed in the Web Worker
DecryptionFailedError
DECRYPTION_FAILED
FHE decryption failed
TransactionRevertedError
TRANSACTION_REVERTED
On-chain transaction reverted (includes failed ERC-20 approvals during shield)
InvalidTransportKeyPairError
INVALID_KEYPAIR
Relayer rejected transport key pair (stale or malformed)
TransportKeyPairExpiredError
KEYPAIR_EXPIRED
Transport key pair expired -- user needs to re-sign
NoCiphertextError
NO_CIPHERTEXT
No encrypted balance exists for this account
RelayerRequestFailedError
RELAYER_REQUEST_FAILED
Relayer HTTP request failed (check .statusCode)
WorkerTimeoutError
OPERATION_TIMEOUT
A worker operation timed out; the Node worker is recycled by default (retryable)
WorkerRecycledError
WORKER_RECYCLED
In-flight op aborted as collateral of another op's timeout recycle (retryable)
ConfigurationError
CONFIGURATION
Invalid SDK config or FHE worker failed to initialize
InsufficientConfidentialBalanceError
INSUFFICIENT_CONFIDENTIAL_BALANCE
Confidential balance too low for transfer or unshield
InsufficientERC20BalanceError
INSUFFICIENT_ERC20_BALANCE
ERC-20 balance too low for shield
BalanceCheckUnavailableError
BALANCE_CHECK_UNAVAILABLE
Balance check impossible (no stored permits)
ERC20ReadFailedError
ERC20_READ_FAILED
Public ERC-20 read failed (network or contract error)
DelegationSelfNotAllowedError
DELEGATION_SELF_NOT_ALLOWED
Delegation cannot target self
DelegationCooldownError
DELEGATION_COOLDOWN
Only one delegate/revoke per tuple per block
DelegationNotFoundError
DELEGATION_NOT_FOUND
No active delegation for this tuple
SignerRequiredError
SIGNER_REQUIRED
Write/sign/decrypt called without a signer
DelegationExpiredError
DELEGATION_EXPIRED
The delegation has expired
SignerNotConfiguredError
SIGNER_NOT_CONFIGURED
SDK operation needs a signer but none is configured (subclass of SignerRequiredError)
WalletNotConnectedError
WALLET_NOT_CONNECTED
Signer exists but has no connected wallet account (subclass of SignerRequiredError)
WalletAccountNotReadyError
WALLET_ACCOUNT_NOT_READY
Async signer adapter hasn't resolved its account yet (subclass of SignerRequiredError)
ChainMismatchError
CHAIN_MISMATCH
Signer and provider are on different chains
DelegationContractIsSelfError
DELEGATION_CONTRACT_IS_SELF
Delegation contract address equals the caller
DelegationDelegateEqualsContractError
DELEGATION_DELEGATE_EQUALS_CONTRACT
Delegate equals the contract address
DelegationExpirationTooSoonError
DELEGATION_EXPIRATION_TOO_SOON
Expiration date less than 1 hour in the future
DelegationExpiryUnchangedError
DELEGATION_EXPIRY_UNCHANGED
New expiry matches the current value
DelegationNotPropagatedError
DELEGATION_NOT_PROPAGATED
Delegated decrypt failed transiently (gateway not synced, or delegator ACL read stale) — retry
AclPausedError
ACL_PAUSED
The ACL contract is paused
2. Catch with instanceof
Use standard try/catch with instanceof to handle specific error types:
Always check the most specific types first and fall back to ZamaError last.
3. Use matchZamaError for cleaner code
Instead of instanceof chains, use matchZamaError to route errors by code:
The _ wildcard catches any ZamaError not explicitly handled. If the error is not a ZamaError at all (and no _ is provided), matchZamaError returns undefined.
Each handler receives the error class for its code, so subclass fields are available without a cast — INSUFFICIENT_CONFIDENTIAL_BALANCE hands you an InsufficientConfidentialBalanceError with .available / .requested, RELAYER_REQUEST_FAILED an error with .statusCode, and so on.
4. Handle specific errors
Here is a quick reference for the most common errors and how to respond:
SigningRejectedError
Show a retry prompt. The user needs to approve the wallet signature.
SigningFailedError
Check wallet connectivity. Hardware wallets may need a firmware update.
EncryptionFailedError
Check your CSP headers -- the Web Worker needs wasm-unsafe-eval.
DecryptionFailedError
May indicate an interrupted unshield. Check for pending state with getPendingUnshield().
TransactionRevertedError
Inspect the revert reason. Common causes: insufficient balance, expired approval.
InvalidTransportKeyPairError
The transport key pair is stale. Clear credentials and prompt for a fresh signature.
TransportKeyPairExpiredError
Same as above -- the transport key pair TTL has elapsed.
NoCiphertextError
Not an error per se. The account has never shielded. Show an empty state in your UI.
RelayerRequestFailedError
Verify relayerUrl in your config. If using API key auth, check the auth option. Inspect .statusCode; on a 429, retry after .retryAfter seconds.
WorkerTimeoutError
Retry with client-side backoff (the Node worker is recycled by default). Raise node({ operationTimeout }) for legitimately long operations.
WorkerRecycledError
Just retry — the request was cancelled as collateral of another operation's timeout recycle, not by a failure of its own.
ConfigurationError
Invalid SDK configuration or FHE worker failed to initialize. Check your transport config and CSP headers.
InsufficientConfidentialBalanceError
Show the user their balance and the shortfall. The operation needs more confidential tokens.
InsufficientERC20BalanceError
Show the user their public token balance. They need more tokens before shielding.
BalanceCheckUnavailableError
Call sdk.permits.grantPermit([token.address]) to sign permits, or pass skipBalanceCheck: true to bypass (useful for smart wallets).
ERC20ReadFailedError
Check network connectivity and RPC endpoint. Retry the shield operation.
SignerRequiredError
Connect a wallet. The operation requires a signer but the SDK was configured without one.
DelegationSelfNotAllowedError
Cannot delegate to yourself. Use a different delegate address.
DelegationCooldownError
Wait for the next block before retrying delegate/revoke on the same tuple.
DelegationNotFoundError
No active delegation exists. Verify the delegator, delegate, and contract addresses.
DelegationExpiredError
The delegation has expired. Create a new delegation.
SignerNotConfiguredError
The SDK was built without a signer. Pass one to createConfig, or connect a wallet.
WalletNotConnectedError
A signer exists but no wallet account is connected. Prompt the user to connect.
WalletAccountNotReadyError
The wallet adapter is still resolving its account. Wait for the connection to settle, then retry.
ChainMismatchError
The wallet is on a different chain than the operation targets. Prompt the user to switch networks.
5. Distinguish "no balance" from "zero balance"
This is a common source of confusion. They require different UI treatments:
See Check Balances for more detail on balance handling patterns.
6. Use matchZamaError in React components
The matchZamaError helper works the same way in React. Here is a reusable error component:
When matchZamaError returns undefined (because the error is not a ZamaError), the component falls back to error.message.
7. Common problems troubleshooting
SigningRejectedError on every decrypt
Wallet rejected the EIP-712 signature
Make sure the wallet supports eth_signTypedData_v4. Some hardware wallets need a firmware update.
Balance always undefined
Encrypted value is zero (never shielded)
Check if the user has shielded tokens first. Catch NoCiphertextError.
ConfigurationError on first operation
FHE worker failed to initialize
Check your CSP headers -- the worker needs wasm-unsafe-eval. Check transport config.
EncryptionFailedError
FHE encryption failed during an operation
Check your CSP headers -- the worker needs wasm-unsafe-eval.
DecryptionFailedError after page reload
Unshield was interrupted
Use getPendingUnshield() on mount to detect and resumeUnshield() to complete it.
TransactionRevertedError on finalize
Unwrap already finalized or tx hash invalid
Check the unwrap tx. If it was already finalized, the unshield is complete -- stop prompting to resume.
RelayerRequestFailedError
Relayer URL wrong or auth missing
Verify relayerUrl in your transport config. If using API key auth, check the auth option.
Next steps
See Error types reference for the full error type reference.
See Hooks for error handling patterns with React Query.
For interrupted unshields specifically, see Unshield Tokens.
Last updated