Errors
Every error this SDK throws extends MorvaSdkError, so catching that one class is enough to catch anything Morva-specific.
import { MorvaSdkError } from "@morva/sdk";
try {
await session.pay(intent, { onStatus });
} catch (err) {
if (err instanceof MorvaSdkError) {
// handle by err.name / instanceof against the specific subclasses below
}
throw err; // don't swallow anything you don't recognize
}Error reference
| Error | Thrown when |
|---|---|
MerchantNotFound | getMerchant / createPaymentIntent against an unregistered address. |
MerchantInactive | Merchant exists but called setActive(false). |
RegistryNotConfigured | A registry call was made without registryAddress in config. |
RegistryDeploymentBlockRequired | getAllMerchants() called without registryDeploymentBlock set — refuses rather than scanning logs from genesis. |
UnsupportedSettlementChain | A settlementChainId (from createDirectIntent or a merchant's registry config) isn't one of SupportedSettlementChainId. |
InsufficientUnifiedBalance | The buyer's unified balance can't cover the intent amount — only checked pre-flight for a known USD stablecoin settlementToken. |
UnroutableBalance | Particle's own routing rejected the transfer (code -32653, “Insufficient primary token balance”) even though the pre-flight check above considered the balance sufficient — Particle doesn't publish the reserve/fee logic behind this. |
UserRejectedSignature | An actual EIP-1193 rejection (code 4001) — a network error or signer bug during signing surfaces as MorvaSdkError instead, never as this. |
SettlementTimeout | No settlement signal within settlementTimeoutMs — genuinely unresolved, not a confirmed failure. |
MorvaSdkError | Base class — also used directly to wrap any unmapped underlying failure, with the original error folded into .message and the original on .cause. |
Handling the ones worth treating differently
Most of these are genuinely exceptional and fine to surface as a generic "payment failed" message. A few are normal outcomes your UI should handle explicitly, not just log:
import { InsufficientUnifiedBalance, UnroutableBalance, SettlementTimeout, UserRejectedSignature } from "@morva/sdk";
try {
await session.pay(intent, { onStatus });
} catch (err) {
if (err instanceof InsufficientUnifiedBalance || err instanceof UnroutableBalance) {
// Same meaning to the buyer ("you can't cover this right now"), two
// different points where it was caught — see the reference table.
} else if (err instanceof SettlementTimeout) {
// err.transactionId is still real — the payment may still land after the timeout.
// Don't tell the buyer it failed; store the id and pass it back in as
// resumeTransactionId on retry (see the callout below).
} else if (err instanceof UserRejectedSignature) {
// A real EIP-1193 rejection — the buyer closed the signature prompt.
// Let them retry; this isn't a system error.
}
}SettlementTimeout is not the same as a failed payment
The error message says this explicitly: the payment may still settle after the timeout elapses. Detecting settlement is a best-effort poll (see how settlement detection works) against a fallback signal, not a guarantee the funds never moved. Never call session.pay() again from scratch for this order — store err.transactionId and pass it back in as resumeTransactionId on the next call, so the SDK resumes checking the same transaction instead of building and submitting a second transfer. Retrying without it can charge the buyer twice for one order if the first transfer does land.