LayerZeroFault
ai agents-api Updated:

Wagmi reconnect() Stuck at 'reconnecting' Error Fix

VV

Written by

Updated:

Technical Guide

The Wagmi reconnect() permanent hang occurs in React and Next.js applications when a stored connector (such as MetaMask or WalletConnect) returns an unhandled rejection inside isAuthorized() or getAccounts() during initial page mount. Because the Wagmi client sets connectionStatus to ‘reconnecting’ before awaiting the connector, an unhandled rejection leaves the state machine permanently locked. Wrapping reconnect() in a timeout fallback restores normal UI execution.

Before executing state changes or signing transactions, verify your cryptographic parameters using our interactive ERC-4337 Calldata Decoder utility.


Architectural Breakdown: The reconnect() Concurrency Trap

Wagmi’s auto-reconnection system runs immediately upon application mounting when reconnectOnMount is enabled in createConfig().

Wagmi v2 & Zustand: Web3 Connection Lifecycle Diagram

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

  1. Permanent Store Lock: The Zustand store retains status: 'reconnecting'. UI components relying on isReconnecting or status === 'connected' render inert skeleton loaders.
  2. Dead Reconnection Loops: Because isReconnecting remains true in module memory, subsequent manual calls via useReconnect().reconnect() return [] instantly without ever querying browser extensions.
  3. Multi-Wallet Clashes: When a user has multiple wallet extensions installed (e.g., MetaMask, Rabby, Coinbase Wallet, Phantom), one extension rejecting with 4100 (Unauthorized) or 4900 (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 CheckInspection Command / ActionExpected Result
Store State ProbeuseAccount().statusMust be 'connected' or cleanly fall back to 'disconnected', never indefinitely 'reconnecting'.
Locked Extension TestLock MetaMask/Rabby, then reload dAppReconnect completes within 1.5s, UI renders “Connect Wallet” button without errors.
Concurrency GuardTrigger reconnect() multiple times rapidlyDoes not throw unhandled promise rejections; returns array or empty list cleanly.
Timeout FailsafeSimulate 10s latency on provider injectionsafeReconnect aborts at 3.5s and demotes status to 'disconnected'.

[!TIP] Diagnostic Verification & Air-Gapped Testing: Before executing state changes or broad recovery steps, cross-reference failure parameters with our companion guide on Wagmi CLI Foundry Plugin Multiple Addresses with Same ABI and verify transaction calldata or cryptographic envelopes using the Master Web3 & RPC Error Code Directory.

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.

Interactive Diagnostic Sandbox

Local Agent API & Calldata Sandboxes

Inspect autonomous agent UserOperations, decode packed execution calldata, and test programmatic authentication payloads locally.

Companion Diagnostic Guide Wagmi CLI Foundry Plugin Multiple Addresses with Same ABI →

Resolve duplicate ABI compilation issues in Wagmi CLI. Learn how to map multiple addresses to a single ABI contract deployment using shared aliases and dynamic runtime addresses.

Read Guide
Contextual Architecture

Related Diagnostic Guides

View All in Silo →

Related Inquiries

Why does Wagmi get stuck at connectionStatus === 'reconnecting'?

When reconnect() executes during SSR hydration, it iterates over previously authorized connectors. If a mobile provider or extension fails to respond or throws an unhandled rejection, Wagmi's internal state machine never transitions to 'disconnected', permanently displaying a loading spinner.

How do I fix the reconnect() hang in Wagmi v2?

Configure reconnectOnMount: false in createConfig and invoke reconnect(config) manually in a useEffect wrapped in a 3-second timeout fallback that forces disconnect() if the connection does not resolve.

Does Next.js strict mode exacerbate the Wagmi reconnect hang?

Yes. React 18/19 Strict Mode mounts and unmounts components twice in development, triggering race conditions where two simultaneous reconnect() promises compete for the same provider instance.

How can I debug smart wallet session reconnection parameters?

Inspect smart account connection calldata and UserOperation parameters using our interactive ERC-4337 Calldata Decoder tool.