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().errorholds the latest failed attempt;statusis"error".connect(id)itself never throws.useConnect().connectAsync(id)anduseWalletManager().connect(id)reject with it, for code that needs the connected wallet as a result.onConnectError(error, connectorId)receives it for every failed attempt.onHydratedreports each failed silent reconnect with aConnectionErroras itsreason.
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
| Kind | Raised when |
|---|---|
UserRejected | EIP-1193 code 4001, or a message containing "user rejected" / "user denied" |
RequestPending | EIP-1193 code -32002: the wallet already has a prompt open |
WalletLocked | a message containing "locked" |
ChainMismatch | a message about a chain "mismatch" or "unsupported" chain; also thrown by Ledger and injected Bitcoin adapters asked for a chain they cannot reach |
NotConnected | EIP-1193 codes 4100 / 4900 / 4901, a "not connected" message, or a wallet that connected but exposed no accounts |
Timeout | a connect that did not settle within 90 seconds, or a silent reconnect within 15 |
WalletNotFound | connect(id) for an id no source announced |
Unknown | anything 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.