butr
Core concepts

Errors

How thrown values from wildly different wallet SDKs become one ConnectionError class you branch on by kind.

Wallet SDKs throw inconsistently: MetaMask uses EIP-1193 numeric codes, Phantom throws stringly-typed errors, embedded SDKs throw their own classes. butr normalises all of it into one class so you branch on kind instead of regexing message strings.

type ConnectionErrorKind =
  | "ChainMismatch"
  | "NotConnected"
  | "RequestPending"
  | "Timeout"
  | "Unknown"
  | "UserRejected"
  | "WalletLocked"
  | "WalletNotFound";

class ConnectionError extends Error {
  readonly kind: ConnectionErrorKind;
  // plus Error's `message`, `name` ("ConnectionError"), and `cause`
}

message is always human-readable. cause keeps the original thrown value, so you can still inspect the wallet's raw error. It is a real Error, so instanceof ConnectionError works and error trackers get a stack.

Where you meet it

  • useConnect().error holds the latest failed attempt; status is "error". connect(id) itself never throws.
  • useConnect().connectAsync(id) and useWalletManager().connect(id) reject with it, for code that needs the connected wallet as a result.
  • onConnectError(error, connectorId) receives it for every failed attempt.
  • onHydrated reports each failed silent reconnect with a ConnectionError as its reason.
import { useConnect } from "@usebutr/react";

const ConnectError = () => {
  const { error, reset } = useConnect();
  if (error === null || error.kind === "UserRejected") {
    return null; // the user clicked "reject"; usually no UI needed
  }
  return (
    <p role="alert">
      {error.kind === "WalletLocked" ? "Unlock your wallet and try again." : error.message}
      <button type="button" onClick={reset}>
        Dismiss
      </button>
    </p>
  );
};

What each kind means

KindRaised when
UserRejectedEIP-1193 code 4001, or a message containing "user rejected" / "user denied"
RequestPendingEIP-1193 code -32002: the wallet already has a prompt open
WalletLockeda message containing "locked"
ChainMismatcha message about a chain "mismatch" or "unsupported" chain; also thrown by Ledger and injected Bitcoin adapters asked for a chain they cannot reach
NotConnectedEIP-1193 codes 4100 / 4900 / 4901, a "not connected" message, or a wallet that connected but exposed no accounts
Timeouta connect that did not settle within 90 seconds, or a silent reconnect within 15
WalletNotFoundconnect(id) for an id no source announced
Unknownanything else

Normalising your own calls

The manager normalises what it catches. Adapter methods you call yourself (signMessage, sendTx, switchChain) reject with whatever the wallet threw. Pass that through toConnectionError to get the same classification:

import { toConnectionError } from "@usebutr/core";

try {
  await wallet.connector.sendTx?.(tx, { account: wallet.account });
} catch (raw) {
  const error = toConnectionError(raw);
  if (error.kind !== "UserRejected") {
    showToast(error.message);
  }
}

It returns a ConnectionError unchanged, wraps any other Error with its classified kind, and turns a non-Error value into Unknown.

Classification is best-effort. A wallet that throws an unrecognised shape lands in Unknown. butr does no retrying; you decide whether to retry based on kind.

Source: packages/core/src/types/errors.ts, packages/core/src/store/wallet-manager.ts.