Error Model

Typed, structured errors with a stable envelope across viem and ethers adapters.

Overview

All SDK operations either:

  1. Throw a ZKsyncError whose .envelope gives you a structured, stable payload, or
  2. 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)

TypeMeaning (how to react)
VALIDATIONInputs are invalid — fix parameters and retry.
STATEOperation not possible yet (e.g., not finalizable). Wait or change state.
EXECUTIONA send/revert happened (tx reverted or couldn’t be confirmed). Inspect revert/cause.
RPCProvider/transport failure. Retry with backoff or check infra.
VERIFICATIONProof/verification issue (e.g., unable to find deposit log).
CONTRACTContract read/encode/allowance failed. Check addresses & ABI.
INTERNALSDK 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 / args appear when decodable; coverage will expand over time.
  • A revert implying “not ready yet” appears as a STATE error 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 err directly prints a compact summary: category, operation, context, optional revert/cause.

[!WARNING] Avoid parsing err.message for logic — use typed fields on err.envelope instead.