Overview
All SDK operations either:
- Throw a
ZKsyncError whose .envelope gives 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.
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
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)
| Type | Meaning (how you should 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 (endpoint/network issue). Retry with backoff / check infra. |
VERIFICATION | Proof/verification step issue. Usually indicates unable to find deposit log. |
CONTRACT | A contract read/encode/allowance failed. Check addresses & ABI compatibility. |
INTERNAL | SDK 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.