BloxwapHyperliquid SDK

Error handling

Typed exceptions thrown by @bloxwap/hyperliquid so you can route error handling by class.

Class hierarchy

Every exception the SDK itself throws extends HyperliquidError. One instanceof check is enough to separate "something in the SDK threw" from "something else threw".

Error
└─ HyperliquidError
   ├─ ValidationError
   ├─ FormatError
   ├─ AbstractWalletError
   ├─ CanonicalizeError
   ├─ ApiRequestError
   └─ TransportError
      ├─ HttpRequestError
      │  └─ HttpRateLimitError
      └─ WebSocketRequestError
ClassThrown fromInspect
ValidationErrorSchema parsing, before any network I/Omessage, cause.issues
FormatErrorformatPrice / formatSize, before network I/Omessage
AbstractWalletErrorSigning layer (viem / custom adapter)cause
CanonicalizeErrorcanonicalize() helper during low-level signingmessage
ApiRequestErrorHyperliquid API returned an error responsemessage, response
HttpRequestErrorfetch failed, non-2xx / non-JSON, or a 200-OK { "type": "error" } enveloperesponse, status, cause
HttpRateLimitErrorServer answered 429 (rate limited)status, retryAfter, cause
WebSocketRequestErrorWebSocket operation failedmessage, cause

Both transport errors also carry a request field with the request payload as it went over the wire: a snapshot of the exact serialization the transport sent, with every signature/signatures value replaced by "0x<redacted>" — at any depth, including multi-sig action.signatures and the { type, payload } envelope WebSocketTransport wraps exchange requests in — so logging or forwarding errors to telemetry never leaks a signature (it reveals trading intent, though never keys). The snapshot is always plain data, never the live object: getters, proxies, and toJSON run exactly once (inside the one serialization the transport computes for sending). A payload that cannot be serialized at all becomes the constant "[unserializable request]".

ValidationError

Thrown when a client method's valibot schema rejects your payload — so it fires before any network I/O. The cause is always a valibot ValiError, and its issues array gives the exact path of every problem.

import { ValidationError } from "@bloxwap/hyperliquid";

try {
  await client.order({ orders: [/* ... */], grouping: "na" });
} catch (error) {
  if (error instanceof ValidationError) {
    console.error(error.message);      // human-readable summary
    console.error(error.cause.issues); // path + expected + received per issue
  }
}

FormatError

Thrown by the formatPrice and formatSize helpers when a value cannot be turned into a valid Hyperliquid price or size — the input is not a finite number, or truncation collapses it to 0.

import { FormatError, formatPrice } from "@bloxwap/hyperliquid/utils";

try {
  formatPrice("not a number", 0);
} catch (error) {
  if (error instanceof FormatError) {
    console.error(error.message); // what was invalid
  }
}

ApiRequestError

Thrown when Hyperliquid's API processed the request and returned an error response. The raw payload is attached as response, typed as the ApiErrorResponse union of the known error shapes:

  • { status: "err", response: string } — the request was rejected outright;
  • { response: { type, data: { statuses: [...] } } } — a bulk action (order, cancel, …) where individual entries carry { error: string };
  • { response: { data: { status: { error: string } } } } — a single-status action (twapOrder, twapCancel);
  • { type: "error", message?: string } — the explorer endpoint's error envelope.

message is the error text extracted from whichever shape matched.

import { ApiRequestError } from "@bloxwap/hyperliquid";

try {
  await client.order({ orders: [/* ... */], grouping: "na" });
} catch (error) {
  if (error instanceof ApiRequestError) {
    console.error(error.message); // server-owned text
    if ("status" in error.response && error.response.status === "err") {
      console.error(error.response.response); // narrowed: top-level error message
    }
  }
}

AbstractWalletError

Thrown from the signing layer when a wallet operation fails: signing EIP-712 typed data, reading the wallet address, or reading the chain id. The underlying wallet's own error (from viem or a custom adapter) is attached as cause.

import { AbstractWalletError } from "@bloxwap/hyperliquid";

try {
  await client.order({ orders: [/* ... */], grouping: "na" });
} catch (error) {
  if (error instanceof AbstractWalletError) {
    console.error(error.message); // SDK-constructed summary
    console.error(error.cause);   // original wallet error
  }
}

CanonicalizeError

Thrown by the canonicalize helper when the payload does not match the schema — an extra field or a missing required field. You only hit this when you are building your own signed action; the built-in ExchangeClient methods never reach this path with their own payloads.

import { CancelRequest } from "@bloxwap/hyperliquid/api/exchange";
import { canonicalize, CanonicalizeError } from "@bloxwap/hyperliquid/signing";

try {
  const action = canonicalize(CancelRequest.entries.action, {
    type: "cancel",
    cancels: [{ a: 0, o: 12345 }],
  });
  // ... continue building the signed payload with `action`
} catch (error) {
  if (error instanceof CanonicalizeError) {
    console.error(error.message); // which key was unexpected or missing
  }
}

Transport errors

TransportError is the common base for every failure that happens at the transport layer. Catch it directly when you want a single branch that covers both HttpRequestError and WebSocketRequestError.

import { TransportError } from "@bloxwap/hyperliquid";

try {
  await client.allMids();
} catch (error) {
  if (error instanceof TransportError) {
    // both HttpRequestError and WebSocketRequestError land here
  }
}

HttpRequestError

Thrown by HttpTransport when fetch itself rejects, when the server returns a non-2xx / non-JSON response, or when a 200-OK body is Hyperliquid's { "type": "error", "message": "..." } failure envelope — some server failures arrive that way instead of as an HTTP error status, and the error's message then carries the server's own text. When the server did respond, response is a Response object — you can read its status and body — and status mirrors the HTTP status code. For network-level failures (DNS, connection reset, offline), both are undefined and the underlying cause is in cause.

import { HttpRequestError } from "@bloxwap/hyperliquid";

try {
  await client.allMids();
} catch (error) {
  if (error instanceof HttpRequestError) {
    if (error.response) {
      console.error(error.status);                // HTTP status
      console.error(await error.response.text()); // response body
    } else {
      console.error(error.cause); // network-level reason
    }
  }
}

The request field holds the original request payload — with a signed payload's signature replaced by "0x<redacted>", as described above.

HttpRateLimitError

Thrown by HttpTransport when the server answers 429 Too Many Requests — the request exceeded Hyperliquid's REST weight budget (1200 weight/minute per IP; repeated violations get the IP banned). It extends HttpRequestError, so existing instanceof HttpRequestError checks keep working; catch the subclass to back off instead of failing. retryAfter carries the server's Retry-After value in seconds, when sent.

import { HttpRateLimitError } from "@bloxwap/hyperliquid";

try {
  await client.allMids();
} catch (error) {
  if (error instanceof HttpRateLimitError) {
    const waitMs = (error.retryAfter ?? 1) * 1000; // seconds the server asked to wait, or a default
    await new Promise((resolve) => setTimeout(resolve, waitMs));
    // retry the request
  }
}

To keep from reaching the limit at all, pace requests client-side with the transport's rateLimit option.

WebSocketRequestError

Thrown by WebSocketTransport when the WebSocket connection cannot be used, or when a request or subscription receives an error response. The underlying cause (if any) is in cause, and the request field holds the original request payload — with a signed payload's signature replaced by "0x<redacted>", as described above.

import { WebSocketRequestError } from "@bloxwap/hyperliquid";

try {
  await client.allMids();
} catch (error) {
  if (error instanceof WebSocketRequestError) {
    console.error(error.message); // SDK-constructed summary
    console.error(error.cause);   // underlying reason, if any
  }
}

Timeouts and cancellation

Both transports use AbortSignal under the hood. The default request timeout (10s) is built with AbortSignal.timeout(), and any signal you pass into a client method is merged with it — whichever aborts first wins. The resulting DOMException is wrapped as cause on a TransportError.

import { TransportError } from "@bloxwap/hyperliquid";

try {
  await client.allMids();
} catch (error) {
  if (error instanceof TransportError && error.cause instanceof DOMException) {
    if (error.cause.name === "TimeoutError") {
      // transport hit the configured timeout
    }
    if (error.cause.name === "AbortError") {
      // caller aborted via their own AbortSignal
    }
  }
}

Catch-all pattern

One pattern that covers every SDK-thrown error, routes by class, and re-throws anything foreign.

import {
  AbstractWalletError,
  ApiRequestError,
  HttpRequestError,
  HyperliquidError,
  TransportError,
  ValidationError,
  WebSocketRequestError,
} from "@bloxwap/hyperliquid";
import { FormatError, formatPrice } from "@bloxwap/hyperliquid/utils";

try {
  const price = formatPrice("65000.1", 3);
  await client.order({ orders: [{ p: price /* ... */ }] });
} catch (error) {
  if (error instanceof HyperliquidError) {
    if (error instanceof ValidationError) {
      // invalid parameters — inspect error.message
    } else if (error instanceof FormatError) {
      // price or size could not be formatted — inspect error.message
    } else if (error instanceof ApiRequestError) {
      // API rejected the action — inspect error.message
    } else if (error instanceof AbstractWalletError) {
      // wallet failed — inspect error.cause
    } else if (error instanceof TransportError) {
      if (error instanceof HttpRequestError) {
        // HTTP transport failed — inspect error.response, error.cause
      } else if (error instanceof WebSocketRequestError) {
        // WebSocket transport failed — inspect error.cause
      }
    } else {
      // Unknown SDK error
    }
  } else {
    throw error; // not ours — let it propagate
  }
}
Edit this page on GitHub

On this page