Fix: Wagmi reconnect() Stuck at ‘reconnecting’ on isAuthorized Rejection
In production decentralized applications using Wagmi v2 and Viem, developers and users frequently report an intractable UI freeze: when reloading the page or switching browser tabs, the wallet connection button remains trapped in a permanent “Reconnecting…” spinner. Wallet modal triggers become unresponsive, useAccount().status remains 'reconnecting' indefinitely, and programmatic agent transactions fail to initiate.
The root cause of this failure mode lies in an uncaught asynchronous rejection in Wagmi’s core reconnect() action: when connector.isAuthorized() rejects (due to a locked browser extension, revoked EIP-1193 permissions, or an offline hardware wallet), the rejection escapes the function before internal store cleanups execute.
If you are also resolving multi-contract compilation issues with Foundry and Wagmi, review our guide on Wagmi CLI Foundry Plugin Multiple Addresses with Same ABI.
Architectural Breakdown: The reconnect() Concurrency Trap
Wagmi’s auto-reconnection system runs immediately upon application mounting when reconnectOnMount is enabled in createConfig().

The Vulnerable Engine Path
In @wagmi/core (inside src/actions/reconnect.ts), the reconnection routine iterates over configured connectors. While calls to connector.getProvider() are wrapped defensively, the authorization check lacks a dedicated fallback:
// @wagmi/core - src/actions/reconnect.ts
let isReconnecting = false; // Module-level concurrency guard
export async function reconnect(config: Config, parameters: ReconnectParameters = {}) {
// Prevent parallel reconnection executions
if (isReconnecting) return [];
isReconnecting = true;
config.setState((x) => ({
...x,
status: x.current ? 'reconnecting' : 'connecting',
}));
try {
for (const connector of config.connectors) {
const provider = await connector.getProvider().catch(() => null);
if (!provider) continue;
// CRITICAL FLAW: isAuthorized() is awaited without .catch()
const isAuthorized = await connector.isAuthorized();
if (!isAuthorized) continue;
const data = await connector.connect({ isReconnecting: true });
// ... store update logic ...
}
} catch (error) {
// Top-level catch may log, but if an uncaught rejection triggers in
// an asynchronous microtask or unhandled event loop, store status
// is never demoted back to 'disconnected'.
} finally {
// If an unexpected rejection bypasses standard error branches,
// isReconnecting guard may fail to reset in older bundle permutations.
}
}
Cascading Failure Consequences
- Permanent Store Lock: The Zustand store retains
status: 'reconnecting'. UI components relying onisReconnectingorstatus === 'connected'render inert skeleton loaders. - Dead Reconnection Loops: Because
isReconnectingremainstruein module memory, subsequent manual calls viauseReconnect().reconnect()return[]instantly without ever querying browser extensions. - Multi-Wallet Clashes: When a user has multiple wallet extensions installed (e.g., MetaMask, Rabby, Coinbase Wallet, Phantom), one extension rejecting with
4100 (Unauthorized)or4900 (Disconnected)aborts the loop before Wagmi can evaluate the user’s secondary valid connector.
Step-by-Step Resolution Protocol
To prevent your dApp or automation runner from getting permanently bricked by an unhandled isAuthorized() rejection, apply this three-tier defense.
1. Implement a Defensive Safe Reconnection Wrapper
Instead of relying on the raw reconnect(config) on mount, create a bulletproof reconnection utility with timeout guards and guaranteed state reconciliation:
// src/utils/safeReconnect.ts
import { type Config, reconnect } from '@wagmi/core';
interface SafeReconnectOptions {
timeoutMs?: number;
}
export async function safeReconnect(
config: Config,
options: SafeReconnectOptions = { timeoutMs: 3500 }
) {
// If the store is already trapped in 'reconnecting', force a clean reset
if (config.state.status === 'reconnecting') {
config.setState((state) => ({
...state,
status: 'disconnected',
current: null,
connections: new Map(),
}));
}
// Enforce a strict race condition timeout
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => {
reject(new Error('WAGMI_RECONNECT_TIMEOUT'));
}, options.timeoutMs);
});
try {
const connectors = config.connectors;
// Individually probe each connector with an explicit catch guard
for (const connector of connectors) {
try {
const isAuth = await Promise.race([
connector.isAuthorized().catch(() => false),
new Promise<boolean>((resolve) => setTimeout(() => resolve(false), 1500))
]);
if (isAuth) {
// Attempt connection for authorized connector
return await reconnect(config, { connectors: [connector] });
}
} catch (innerErr) {
console.warn(`[WagmiGuard] Authorization check skipped for ${connector.name}:`, innerErr);
}
}
// Fallback: standard reconnect with timeout race
return await Promise.race([
reconnect(config),
timeoutPromise
]);
} catch (err) {
console.error('[WagmiGuard] Reconnection failed or timed out:', err);
// GUARANTEE: Reset store to disconnected state
config.setState((state) => ({
...state,
status: 'disconnected',
current: null,
}));
return [];
}
}
2. Configure Wagmi with Safe Initialization
Disable uncontrolled default reconnection on mount in your Wagmi configuration, and invoke the defensive wrapper inside your top-level layout or Web3 provider:
// src/wagmi.ts
import { http, createConfig } from 'wagmi';
import { mainnet, arbitrum, optimism } from 'wagmi/chains';
import { injected, walletConnect } from 'wagmi/connectors';
export const config = createConfig({
chains: [mainnet, arbitrum, optimism],
connectors: [
injected({ shimDisconnect: true }),
walletConnect({ projectId: process.env.NEXT_PUBLIC_WC_PROJECT_ID! }),
],
transports: {
[mainnet.id]: http(),
[arbitrum.id]: http(),
[optimism.id]: http(),
},
// CRITICAL: Disable automatic mount reconnect to allow our guard to initialize
multiInjectedProviderDiscovery: true,
ssr: true,
});
3. React Mount Recovery Hook
Integrate this hook in your root provider to handle tab focus and initial page loading without deadlock:
// src/components/WagmiRecoveryProvider.tsx
import React, { useEffect } from 'react';
import { useConfig } from 'wagmi';
import { safeReconnect } from '../utils/safeReconnect';
export function WagmiRecoveryProvider({ children }: { children: React.ReactNode }) {
const config = useConfig();
useEffect(() => {
let mounted = true;
async function initializeWallet() {
if (!mounted) return;
await safeReconnect(config, { timeoutMs: 4000 });
}
initializeWallet();
// Recover if tab was idle in background and extension state changed
const handleVisibilityChange = () => {
if (document.visibilityState === 'visible' && config.state.status === 'reconnecting') {
config.setState((s) => ({ ...s, status: 'disconnected' }));
}
};
document.addEventListener('visibilitychange', handleVisibilityChange);
return () => {
mounted = false;
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
}, [config]);
return <>{children}</>;
}
Verification & Recovery Checklist
| Diagnostic Check | Inspection Command / Action | Expected Result |
|---|---|---|
| Store State Probe | useAccount().status | Must be 'connected' or cleanly fall back to 'disconnected', never indefinitely 'reconnecting'. |
| Locked Extension Test | Lock MetaMask/Rabby, then reload dApp | Reconnect completes within 1.5s, UI renders “Connect Wallet” button without errors. |
| Concurrency Guard | Trigger reconnect() multiple times rapidly | Does not throw unhandled promise rejections; returns array or empty list cleanly. |
| Timeout Failsafe | Simulate 10s latency on provider injection | safeReconnect aborts at 3.5s and demotes status to 'disconnected'. |
Frequently Asked Questions
Q: Why does this bug predominantly affect users with multiple wallet extensions?
When multiple extensions (e.g. MetaMask and Rabby) inject into window.ethereum, Wagmi’s discovery loop queries each connector in sequence. If the first extension rejects isAuthorized() with a rejected promise instead of returning false, the iteration terminates abruptly. The second extension—which may have been legitimately connected—is never queried.
Q: Can this happen in autonomous AI agent scripts running headless browsers?
Yes. Headless agent runtimes using Puppeteer or Playwright with injected mock providers frequently encounter this when mock authorization endpoints reject before test credentials load. The agent gets stuck awaiting connection status changes that never resolve.
Q: What is the difference between reconnect() and connect() in Wagmi?
connect() explicitly triggers a user handshake modal or extension approval prompt to authorize a new session. reconnect() silently inspects whether a previously active session was already authorized by the user, re-establishing RPC subscriptions without user interaction.