LazorKit LogoLazorKit

Errors

Every error class the LazorKit SDKs throw, when each is thrown and what to do, and the program's error codes 3001–3036 and 4001–4018.

One reference for the React SDK (@lazorkit/wallet 3.3.0), the React Native SDK (@lazorkit/wallet-mobile-adapter 2.3.0) and the contract SDKs (@lazorkit/sdk-legacy 1.3.1, @lazorkit/sdk 1.0.0-rc.5). Symptom-first help is in Troubleshooting.

Telling errors apart

Match the SDK's own error classes with instanceof:

import {
  SignatureReusedError,
  TransactionOutcomeUnknownError,
  V1WalletRetiredError,
} from '@lazorkit/wallet'; // or '@lazorkit/wallet-mobile-adapter'

function describe(e: unknown): string {
  if (e instanceof TransactionOutcomeUnknownError) return 'Not sure it landed: check before sending again';
  if (e instanceof SignatureReusedError) return 'Please approve again';
  if (e instanceof V1WalletRetiredError) return 'Move this wallet to v2';
  return 'Something went wrong';
}

instanceof fails when your bundle holds two copies of the package (an ESM and a CJS build, or two versions), and when the error is wrapped: through the Wallet Standard, a failed send reaches a dApp as wallet-adapter's WalletSendTransactionError, with the LazorKit error in its error field. The predicates cover both cases:

PredicateTrue for
isSignatureReusedError(e)SignatureReusedError, and a raw 3006 (one with no logs counts as LazorKit's)
isDeferredExpiredError(e)DeferredExpiredError, and a raw 3014 whose logs name LazorKit as the first program to fail
isRetiredDeploymentError(e, version?)V1WalletRetiredError, and a raw 4018 whose logs name the v1 program, or any 4018 when version is 1
isKeyWalletMismatchError(e) (web)KeyWalletMismatchError: see Kept session and authority keys

Each matches its class from any copy of the package (by name and code), anywhere in the error's cause chain or inside a wallet-adapter WalletError. For a raw program error they read the message, the logs, a paymaster's data and a TransactionError, in the error and in its causes. The other classes have no predicate: compare error.name, which holds across copies too.

Changed in 3.3.0 / adapter 2.3.0

In @lazorkit/wallet 3.2.1 and @lazorkit/wallet-mobile-adapter 2.2.1, isSignatureReusedError and isRetiredDeploymentError read only the error's text, so they were false for the SDK's own SignatureReusedError and V1WalletRetiredError, and no predicate looked inside cause or a WalletError.

Sending a transaction

Thrown by every send: signAndSendTransaction, authorizeAndExecute, authorizeDeferred, executeDeferred, the session and authority sends, transferSol and reclaimDeferred (mobile), LazorkitWalletAdapter.sendTransaction and the Wallet Standard signAndSendTransaction (web). Exported by both UI packages. Since @lazorkit/wallet 3.1.0 / @lazorkit/wallet-mobile-adapter 2.1.0.

ErrorThrown whenCarriesWhat to do
TransactionFailedErrorThe transaction landed and failed. Fees were paid; nothing else changed.signature, transactionError, slot, logs (when read)Read logs for the failing program and code. Fix the cause before retrying.
TransactionExpiredErrorIt did not land before its blockhash expired, so it never will. Concluded only from a node past that point, in its transaction history.signatureSafe to build and send again.
TransactionOutcomeUnknownErrorWhether it landed is not known: the paymaster's answer was lost (network error, timeout, gateway error; on web, after the SDK resent the same signed bytes, up to 3 attempts in all), or no status could be read.signature (undefined when the answer was lost)Do not resend blindly. Check signature, or the state the transaction would change, first.
ConfirmationTimeoutErrorA TransactionOutcomeUnknownError: no outcome within two minutes, and the blockhash has not expired. It may still land.signature, waitedMsAs above. Catching TransactionOutcomeUnknownError covers it.
PreviousTransactionPendingErrorNothing was signed or sent: this passkey's previous transaction still has no known outcome, and a new signature could reuse its counter.pendingSignatureTry again once the previous one settles.
SignatureReusedErrorLazorKit rejected the passkey signature with SignatureReused (3006): its counter was already used. Usually the same passkey signing elsewhere at the same moment (another tab or device), or a paymaster reading older state.code: 3006The signature can never become valid and is not resent. Ask the user to approve again.
PaymasterErrorThe paymaster refused the request.code, data (JSON-RPC error), httpStatus, signature, maybeSentRead code / data. A simulation failure usually carries the program logs in data.
DeferredExpiredErrorTX2 of a deferred execution came after its authorization expired (3014). Nothing in the payload ran; the approval is spent.deferredExecPda, authorizeSignature, expiresAtSlot, code: 3014Ask the user to approve again. The paymaster's fee payer can reclaim the rent with ReclaimDeferred.

A 3006 or 3014 that an inner program returned (Anchor's AccountNotMutable is 3006, AccountNotAssociatedTokenAccount is 3014) is not reported as SignatureReusedError or DeferredExpiredError: the SDK reads the logs, and throws the error as it came.

Any error from sending TX2 of a deferred execution also carries the DeferredFailureContext fields: deferredExecPda, authorizeSignature (when the call sent TX1) and expiresAtSlot (when it was read). Since 3.2.1 / 2.2.1.

How sends are confirmed and sequenced: React › Sending transactions and React Native › Sending transactions.

Connecting and the portal

ErrorPackageThrown whenWhat to do
WalletNeedsConfirmationErrorbothonConfirmWallet: 'throw' and the passkey's wallet needs the user. Carries credentialId and candidates: WalletChoice[].Show the vaults; call connect({ confirmWallet }) with the user's pick.
WalletConfirmationDeclinedErrorbothThe user chose none of the offered wallets. Nothing was saved or created.Do not create a wallet. Let the user try another passkey.
PortalCancelledErrorbothThe user closed the portal (web: the dialog's X, Escape, a click outside, the popup; mobile: dismissing the browser) before it answered, or disconnect was called while connect ran. Also on every signing action.Nothing was signed. Treat as a cancel.
WalletChooserNotShownErrormobileiOS could not show the built-in chooser over another open modal.Mount <WalletChooser /> inside your modal, or close it first.
LazorKitError, code: 'PORTAL_ERROR'mobileThe portal redirected with an error; its text is the message.Show the message.
WalletConnectionErrormobileA second connect while one is running ("Already connecting"), or a redirect that carried no valid wallet.Wait for the first connect.
Error('Already connecting')webA second connect from the store while one is running.Wait for the first connect.

A connect that creates a wallet for a passkey whose key the portal cannot report asks for one more signature to recover the key (since 3.1.0 / 2.1.0). If the signatures do not settle on one key, or a portal reply names another passkey, connect throws a plain Error that says so, and nothing is created.

Signing while another call runs

PackageErrorWhen
webError('Already signing')A store action (every hook method) while another one is still signing or confirming. isSigning stays true until the transaction is confirmed.
mobileSigningErrorSame rule: a sign action while another is running. Also "No wallet connected".

Both SDKs report these refusals to the action's onFail as well. A refusal because another call is running calls onFail at once and leaves isSigning and error to the running call; "No wallet connected" also sets error. (Web since 3.3.0; on mobile, transferSol with no wallet connected since 2.3.0.)

Calls through LazorkitWalletAdapter and the Wallet Standard wallet are not refused: they queue behind the passkey's running transaction.

Kept session and authority keys (web)

@lazorkit/wallet keeps each key createSession and addAuthority generate with the wallet it was registered for, and uses it only while that wallet is connected. Otherwise the call rejects before anything is signed or sent:

ErrorThrown byCarriesWhat to do
KeyWalletMismatchError (code: 'KEY_WALLET_MISMATCH')signAndSendWithSession, signAndSendWithAuthority, and revokeSession() without sessionPda, when the kept key's wallet is not the connected one. The key checks again when it signs, so a disconnect or a switch while a send is being built stops it too. onFail runs once isSigning is false.slot ('session' or 'authority'), reason, keyWallet (the key's wallet PDA), connectedWallet (the connected one, undefined when none)'no-wallet' or 'other-wallet': connect keyWallet, or create a session (add an authority) for the connected wallet. 'unbound': a key whose wallet could not be confirmed (one 3.2 left, or a stored record whose PDA does not derive from the wallet it names). It is never used: create the session (add the authority) again, which replaces it. forgetStoredKeys() would delete the other kept key too.

Match it with isKeyWalletMismatchError(e). A kept session key whose session has expired is deleted when it is next read, and the call rejects with "No session key found: the stored session … expired after slot …": create a new session. Where the keys are kept and when they are deleted: What the SDK stores.

Changed in 3.3.0

In 3.2.1, signAndSendWithSession and signAndSendWithAuthority signed with the stored key whichever wallet was connected, or none. See Upgrading to 3.3.0.

v1 and v2

ErrorThrown whenWhat to do
V1WalletRetiredError (code: 4018)The wallet is on LazorKit v1, which has been retired to its sunset binary. Its funds are safe.Move it with migrateV1Wallet or the LazorKit migration page. See Migrating from v1.
V1WalletMigratedErrorThe stored v1 wallet no longer exists: it was migrated (or closed).Call connect again; it picks up the passkey's v2 wallet.

A 4018 is never resent: the call fails on the paymaster's first answer. Both SDKs report a v1 wallet's 4018 as V1WalletRetiredError, with the original error as its cause. A web Paymaster you call directly throws V1WalletRetiredError when the logs name the v1 program or it was built with { protocolVersion: 1 }, and the PaymasterError otherwise.

Changed in 3.3.0

In 3.2.1 the web paymaster resent a 4018 like other failures, up to 3 attempts 1 s and 2 s apart, although a retired program answers each attempt the same way. The React Native SDK never resent it.

Input checks (thrown before any prompt)

Thrown byErrorCause
createSession (all SDKs)ErrorNo limits and no unrestricted: true. A session without actions can spend the whole vault.
addAuthority / addAuthorityEd25519ErrorNo role, or one that is not ROLE_OWNER (0), ROLE_ADMIN (1) or ROLE_SPENDER (2); ROLE_OWNER on a v2 wallet, where neither method adds an Owner (on v1 they do). The message says what each rank may do. Checked before anything is read.
addAuthority / addAuthorityEd25519ErrorROLE_SPENDER (Delegate) on a v2 wallet without a policy; a policy on a v1 wallet; a v1 wallet without unrestricted: true.
authorizeAndExecute / authorizeDeferredRangeErrorexpiryOffset outside 10–9000 slots.
new LazorKitClient(connection) (sdk-legacy)ErrorThe RPC URL names no cluster. Pass a program id.
migrateV1WalletErrorThe client was built at a v1 id, or the destination failed its vet.

Changed in 3.3.0 / adapter 2.3.0

Breaking: addAuthority and addAuthorityEd25519 have no default role any more (3.2.1 made the key an Admin, 2.2.1 a Delegate). See Ranks & policies.

Contract SDKs

ErrorPackageThrown when
MinContextSlotNotReachedError@lazorkit/sdk-legacy ≥ 1.3.0, @lazorkit/sdk ≥ rc.4 (also re-exported by the mobile adapter)A challenge read with minContextSlot found no node at that slot within 10 s (30 s at finalized). Carries minContextSlot, waitedMs, commitment. Retry on an RPC that has caught up. See Passkey signing.

The contract SDKs do not wrap program errors: a failed transaction surfaces as web3.js's SendTransactionError (or kit's equivalent) with the custom program error in its message and logs.

Wallet Standard and wallet-adapter

signTransaction and signAllTransactions are not supported and throw WalletSignTransactionError. Use sendTransaction / signAndSendTransaction. When LazorKit is used through the Wallet Standard (registerLazorkitWallet), wallet-adapter's standard-wallet adapter wraps a failed send in WalletSendTransactionError, with the LazorKit error in its error field: check e.error instanceof …, not e, or use the is*Error predicates, which read through it (3.3.0+).

Program error codes

The program returns these as custom program error: 0x… (hex) or { "Custom": N } (decimal).

Is it LazorKit's code?

Programs that Execute calls can fail with the same numbers: Anchor's account errors use 3000–3017. The first log line Program <id> failed: custom program error: 0x… names the program that raised it; only when <id> is the LazorKit program is the code one of these. A landed failure ({ InstructionError: [i, { Custom: N }] }) names only the top-level LazorKit instruction, so read its logs (TransactionFailedError.logs, or getTransaction(signature) → meta.logMessages) before acting on it.

Authentication and permissions (3001–3019)

CodeHexNameMeaning
30010xbb9InvalidAuthorityPayloadThe auth payload is malformed (lengths, authenticatorData, clientDataJSON).
30020xbbaPermissionDeniedThe signer's rank or policy does not allow this instruction, or it was called through CPI.
30030xbbbInvalidInstructionInstruction data could not be decoded.
30040xbbcInvalidPubkeyA key does not match the expected PDA or authority.
30050xbbdInvalidMessageHashThe signed challenge differs from what the program recomputed: wrong accounts, wrong payload, or a client still building the challenge without the wallet (v2 names the wallet in it).
30060xbbeSignatureReusedThe passkey signed a counter that was already used. See SignatureReusedError above.
30070xbbfInvalidSignatureAgeThe challenge's slot is in the future or 150 or more slots old. Approve and send faster, or read the slot at a lower commitment.
30080xbc0InvalidSessionDurationSession expiry not in the future, or more than 6,480,000 slots ahead.
30090xbc1SessionExpiredThe session's expiry slot has passed. Create a new session.
30100xbc2AuthorityDoesNotSupportSessionOnly an Owner or Admin may create or revoke sessions.
30110xbc3InvalidAuthenticationKindThe authentication type does not match the authority (Ed25519 vs Secp256r1).
30120xbc4InvalidMessageThe signed payload could not be parsed.
30130xbc5SelfReentrancyNotAllowedAn inner instruction called back into LazorKit.
30140xbc6DeferredAuthorizationExpiredTX2 came after the authorization's window. See DeferredExpiredError.
30150xbc7DeferredHashMismatchTX2's instructions or accounts differ from what TX1 authorized, or TX2 is sent by another executor than the one hashed (see executor in Passkey signing).
30160xbc8InvalidExpiryWindowexpiryOffset outside 10–9000 slots.
30170xbc9UnauthorizedReclaimOnly the payer that funded the DeferredExec may reclaim it, and only to itself.
30180xbcaDeferredAuthorizationNotExpiredReclaim attempted before the window ended.
30190xbcbInvalidSessionAccountThe session account does not match the session key or wallet.

Spending limits and policies (3020–3036)

These apply to session keys and to Delegate authorities, which run the same action engine.

CodeHexNameMeaning
30200xbccActionBufferInvalidThe actions buffer is malformed. Build it with Actions.* / serializeActions.
30210xbcdActionProgramNotWhitelistedAn inner instruction targets a program outside the whitelist.
30220xbceActionProgramBlacklistedAn inner instruction targets a blacklisted program.
30230xbcfActionSolMaxPerTxExceededSOL outflow in one execute exceeds SolMaxPerTx.
30240xbd0ActionSolLimitExceededThe lifetime SOL cap is spent. Limits cannot be changed: revoke and create a new session.
30250xbd1ActionSolRecurringLimitExceededThe SOL cap for the current window is spent. Wait for the window to reset.
30260xbd2ActionTokenLimitExceededThe lifetime token cap is spent.
30270xbd3ActionTokenRecurringLimitExceededThe token cap for the current window is spent.
30280xbd4ActionWhitelistBlacklistConflictThe same program is both whitelisted and blacklisted.
30290xbd5ActionTokenMaxPerTxExceededToken outflow in one execute exceeds TokenMaxPerTx.
30300xbd6SessionVaultOwnerChangedAn inner instruction tried to reassign the vault (System::Assign).
30310xbd7SessionVaultDataLenChangedAn inner instruction changed the vault's data length.
30320xbd8SessionTokenAuthorityChangedAn inner instruction changed a vault token account's owner, delegate or close authority.
30330xbd9DelegateRequiresPolicyA Delegate (ROLE_SPENDER) was added without a policy.
30340xbdaPolicyBearingAuthorityCannotDelegateAn authority with a policy tried to add an authority.
30350xbdbPolicyRankMismatchA policy was attached to an Owner or Admin. Only Delegates carry one.
30360xbdcSessionNotExpiredCloseExpiredSession on a session that is still live.

Protocol and deployment (4001–4018)

CodeHexNameMeaning
40010xfa1ProtocolAlreadyInitializedInitializeProtocol ran twice.
40020xfa2InvalidProtocolAdminThe signer is not the protocol admin.
40040xfa4InvalidIntegratorRecordThe fee record is not valid.
40050xfa5InsufficientFeeBalanceThe fee payer cannot pay the protocol fee.
40060xfa6IntegratorAlreadyRegisteredRegisterPayer for a payer that already has a FeeRecord.
40070xfa7InvalidTreasuryThe treasury account is not the configured one.
40080xfa8FeeAccountsRequiredA fee-eligible instruction (CreateWallet, Execute, ExecuteDeferred) came without the four fee accounts. v2 requires them even when no fee is charged: an old SDK, or a hand-built instruction.
40090xfa9ProtocolNotInitializedThe ProtocolConfig account is missing or malformed.
40100xfaaInvalidTreasuryShardThe treasury shard is not one of this program's.
40110xfabInvalidFeeRecordThe FeeRecord is not the canonical one for this payer.
40130xfadAccountVersionMismatchAn account was written by another build of the program.
40140xfaeFeeExceedsMaximumA protocol fee above 0.01 SOL was configured.
40150xfafUnauthorizedInitializerInitializeProtocol signed by a key other than the one compiled in.
40160xfb0NoPendingAdminAdmin rotation accepted by a key that is not the pending admin.
40170xfb1WrongProgramAddressThe binary runs at an address other than the one compiled into it.
40180xfb2RetiredDeploymentA retired v1 program answers everything except ReclaimDeferred, MigrateWallet and CloseExpiredSession with this. See V1WalletRetiredError.

4003 (ProtocolDisabled) and 4012 (FeeNotConfigured) are retired: v2 skips fee collection when the protocol is not configured, instead of failing.

Precompile errors

Low numbers (0x0–0x5) come from Solana's signature-verification precompiles. The common one is 0x2 from Secp256r1SigVerify…: the passkey signature does not verify against the key and message. Usually a stale public key or a modified clientDataJSON; see Troubleshooting.

Decoding helpers

import { ERROR_NAMES, errorFromCode, extractErrorCode } from '@lazorkit/sdk-legacy';
// also re-exported by '@lazorkit/wallet' and '@lazorkit/wallet-mobile-adapter'

declare const err: unknown;
const code = extractErrorCode(err);                 // e.g. 3024, or null
const name = code === null ? undefined : errorFromCode(code); // 'ActionSolLimitExceeded'
console.log(code, name, Object.keys(ERROR_NAMES).length);

extractErrorCode reads the error's text only: it returns null for a { InstructionError: … } object. In @lazorkit/sdk-legacy 1.3.1, ERROR_NAMES has no entry for 3036 or 4018, so errorFromCode returns undefined for them; use the tables above.