Skip to main content

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.
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 '@dutterbutter/zksync-sdk/core';

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;

    // Example: route on category
    switch (type) {
      case 'VALIDATION':
      case 'STATE':
        // user/action fixable (bad input, not-ready, etc.)
        break;
      case 'EXECUTION':
      case 'RPC':
        // network/tx/provider issues
        break;
    }

    // Optional: log structured data
    console.error(JSON.stringify(err.toJSON()));
  } else {
    // Non-SDK error (framework, userland)
    throw e;
  }
}

Envelope shape

name
'ZKsyncError'
Instance type for all SDK-thrown errors.

ZKsyncError.envelope: ErrorEnvelope

type ErrorEnvelope = {
  /** Resource surface that raised the error. */
  resource: 'deposits' | 'withdrawals' | 'withdrawal-finalization' | 'helpers' | 'zksrpc';

  /** Specific operation, e.g. "withdrawals.finalize" or "deposits.create". */
  operation: string;

  /** Broad category (see table below). */
  type: 'VALIDATION' | 'STATE' | 'EXECUTION' | 'RPC' | 'INTERNAL' | 'VERIFICATION' | 'CONTRACT';

  /** Stable, human-readable message for developers. */
  message: string;

  /** Optional contextual fields (tx hash, nonce, step key, etc.). */
  context?: Record<string, unknown>;

  /** If the error is a contract revert, adapters include decoded info when available. */
  revert?: {
    selector: `0x${string}`; // 4-byte selector
    name?: string; // Decoded Solidity error name
    args?: unknown[]; // Decoded args
    contract?: string; // Best-effort contract label
    fn?: string; // Best-effort function label
  };

  /** Originating error (provider/transport/etc.), sanitized for safe logging. */
  cause?: unknown;
};

Categories (when to expect them)

TypeMeaning (how you should 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 (endpoint/network issue). Retry with backoff / check infra.
VERIFICATIONProof/verification step issue. Usually indicates unable to find deposit log.
CONTRACTA contract read/encode/allowance failed. Check addresses & ABI compatibility.
INTERNALSDK internal error (please report with operation + selector if present).

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) {
  // res.error is a ZKsyncError
  console.warn(res.error.envelope.message, res.error.envelope.operation);
} else {
  // res.value is the success payload
  console.log('l2TxHash', res.value.l2TxHash);
}
This is especially handy for UI flows where you want to surface inline validation/state messages without a try/catch.

Revert details (when transactions fail)

If the provider exposes revert data, the adapters will 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 against known ABIs; coverage will expand over time.
  • When a revert implies “not ready yet,” you’ll typically see a STATE error with a clarifying message.

Ethers & viem examples

import { JsonRpcProvider, Wallet } from 'ethers';
import { createEthersClient, createEthersSdk } from '@dutterbutter/zksync-sdk/ethers';
import { isZKsyncError } from '@dutterbutter/zksync-sdk/core';

const l1 = new JsonRpcProvider(process.env.ETH_RPC!);
const l2 = new JsonRpcProvider(process.env.ZKSYNC_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({ token, amount, to });
if (!res.ok) {
// report envelope
console.error(res.error.envelope);
}

Logging & observability

  • err.toJSON() returns a safe, structured object you can ship to logs/telemetry.
  • For local debugging, printing err shows a compact, human-readable view (category, operation, context, optional revert/cause).
Avoid parsing err.message for logic. Use the typed fields on err.envelope instead.