Error Model
Typed, structured errors with a stable envelope across viem and ethers adapters.
Overview
All SDK operations either:
- Throw a
ZKsyncErrorwhose.envelopegives you a structured, stable payload, or - Return a result object from the
try*variants:{ ok: true, value } | { ok: false, error }
This is consistent across both ethers and viem adapters.
[!TIP] Prefer the
try*variants when you want to avoid exceptions and branch on success/failure.
What Gets Thrown
When the SDK throws, it throws an instance of ZKsyncError.
Use isZKsyncError(e) to narrow and read the error envelope.
import { isZKsyncError } from '@matterlabs/zksync-js/core/types/errors';
try {
const handle = await sdk.deposits.create(params);
} catch (e) {
if (isZKsyncError(e)) {
const err = e; // type-narrowed
const { type, resource, operation, message, context, revert } = err.envelope;
switch (type) {
case 'VALIDATION':
case 'STATE':
// user/action fixable (bad input, not-ready, etc.)
break;
case 'EXECUTION':
case 'RPC':
// network/tx/provider issues
break;
}
console.error(JSON.stringify(err.toJSON())); // structured log
} else {
throw e; // non-SDK error
}
}
Envelope Shape
Instance Type
'ZKsyncError'
ZKsyncError.envelope: ErrorEnvelope
interface ErrorEnvelope {
/** Resource surface that raised the error. */
resource: Resource;
/** SDK operation, e.g. 'withdrawals.finalize' */
operation: string;
/** Broad category */
type: ErrorType;
/** Human-readable, stable message for developers. */
message: string;
/** Optional detail that adapters may enrich (reverts, extra context) */
context?: Record<string, unknown>;
/** If the error is a contract revert, adapters add decoded info here. */
revert?: {
/** 4-byte selector as 0x…8 hex */
selector: `0x${string}`;
/** Decoded error name when available (e.g. 'InvalidProof') */
name?: string;
/** Decoded args (ethers/viem output), when available */
args?: unknown[];
/** Optional adapter-known labels */
contract?: string;
/** Optional adapter-known function name */
fn?: string;
};
/** Original thrown error */
cause?: unknown;
}
Categories (When to Expect Them)
| Type | Meaning (how to react) |
|---|---|
VALIDATION | Inputs are invalid — fix parameters and retry. |
STATE | Operation not possible yet (e.g., not finalizable). Wait or change state. |
EXECUTION | A send/revert happened (tx reverted or couldn’t be confirmed). Inspect revert/cause. |
RPC | Provider/transport failure. Retry with backoff or check infra. |
VERIFICATION | Proof/verification issue (e.g., unable to find deposit log). |
CONTRACT | Contract read/encode/allowance failed. Check addresses & ABI. |
INTERNAL | SDK internal issue — report with operation and selector. |
Result Style (try*) Helpers
Every resource method has a try* sibling that never throws and returns a TryResult<T>.
const res = await sdk.withdrawals.tryCreate(params);
if (!res.ok) {
if (isZKsyncError(res.error)) {
// res.error is a ZKsyncError
console.warn(res.error.envelope.message, res.error.envelope.operation);
} else {
throw new Error("Unkown error");
}
} else {
console.log('l2TxHash', res.value.l2TxHash);
}
This is especially useful for UI flows where you want inline validation/state messages without try/catch.
Revert Details (When Transactions Fail)
If the provider exposes revert data, the adapters decode common error types and ABIs so you can branch on them:
try {
await sdk.withdrawals.finalize(l2TxHash);
} catch (e) {
if (isZKsyncError(e) && e.envelope.revert) {
const { selector, name, args } = e.envelope.revert;
// e.g., name === 'InvalidProof' or 'TransferAmountExceedsBalance'
}
}
Notes
- The SDK always includes the 4-byte selector.
name/argsappear when decodable; coverage will expand over time.- A revert implying “not ready yet” appears as a
STATEerror with a clear message.
Ethers & Viem Examples
Ethers
import { JsonRpcProvider, Wallet } from 'ethers';
import { createEthersClient, createEthersSdk } from '@matterlabs/zksync-js/ethers';
import { isZKsyncError } from '@matterlabs/zksync-js/core/types/errors';
const l1 = new JsonRpcProvider(process.env.L1_RPC!);
const l2 = new JsonRpcProvider(process.env.L2_RPC!);
const signer = new Wallet(process.env.PRIVATE_KEY!, l1);
const client = createEthersClient({ l1, l2, signer });
const sdk = createEthersSdk(client);
const res = await sdk.deposits.tryCreate(params);
if (!res.ok) {
if (isZKsyncError(res.error)) console.error(res.error.envelope); // structured envelope
}
Viem
import { JsonRpcProvider, Wallet, parseEther } from 'ethers';
import { createEthersClient, createEthersSdk } from '@matterlabs/zksync-js/ethers';
import { ETH_ADDRESS } from '@matterlabs/zksync-js/core';
import { isZKsyncError } from '@matterlabs/zksync-js/core/types/errors';
const l1 = new JsonRpcProvider(process.env.L1_RPC!);
const l2 = new JsonRpcProvider(process.env.L2_RPC!);
const signer = new Wallet(process.env.PRIVATE_KEY!, l1);
const client = createEthersClient({ l1, l2, signer });
const sdk = createEthersSdk(client);
const res = await sdk.deposits.tryCreate(params);
if (!res.ok) {
if (isZKsyncError(res.error)) console.error(res.error.envelope); // structured envelope
}
Logging & Observability
err.toJSON()→ returns a safe, structured object suitable for telemetry.- Logging
errdirectly prints a compact summary: category, operation, context, optional revert/cause.
[!WARNING] Avoid parsing
err.messagefor logic — use typed fields onerr.envelopeinstead.