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

ErrorThrown when
MerchantNotFoundgetMerchant / createPaymentIntent against an unregistered address.
MerchantInactiveMerchant exists but called setActive(false).
RegistryNotConfiguredA registry call was made without registryAddress in config.
RegistryDeploymentBlockRequiredgetAllMerchants() called without registryDeploymentBlock set — refuses rather than scanning logs from genesis.
UnsupportedSettlementChainA settlementChainId (from createDirectIntent or a merchant's registry config) isn't one of SupportedSettlementChainId.
InsufficientUnifiedBalanceThe buyer's unified balance can't cover the intent amount — only checked pre-flight for a known USD stablecoin settlementToken.
UnroutableBalanceParticle'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.
UserRejectedSignatureAn actual EIP-1193 rejection (code 4001) — a network error or signer bug during signing surfaces as MorvaSdkError instead, never as this.
SettlementTimeoutNo settlement signal within settlementTimeoutMs — genuinely unresolved, not a confirmed failure.
MorvaSdkErrorBase 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.