Skip to content

tempo: getAccountsModule chains .catch() on import(), breaking under bundlers where dynamic import isn't a spec Promise (e.g. Metro/React Native) #5201

Description

@christopherwxyz

Describe the bug

The Tempo webAuthn/tempoWallet/dangerous_secp256k1 connectors in @wagmi/core's tempo module lazily load the peer-installed accounts package via:

async function getAccountsModule() {
  return await import('accounts').catch(() => {
    throw new Error('dependency "accounts" not found')
  })
}

Chaining .catch() directly on the return value of a dynamic import() assumes that value is always a spec-compliant native Promise. That's not guaranteed in every bundling environment — notably under Metro (React Native's bundler), particularly with eager/non-lazy bundling (?lazy=false, which some setups force to work around unrelated issues), the object returned by a transformed import() call reliably has .then() but does not have a .catch() method.

The result is a very misleading failure: instead of surfacing "dependency accounts not found" (or succeeding, since accounts is installed and resolvable), the app crashes with:

TypeError: undefined is not a function

thrown from evaluating import('accounts').catch itself — before the import even settles, and before the intended catch handler's custom error message ever has a chance to run. This makes the connector effectively unusable in this class of environment, and the actual error message is unhelpful for diagnosing why.

Confirmed via runtime introspection in the affected environment:

const importResult = import('accounts')
typeof importResult        // "object"
typeof importResult.then   // "function"
typeof importResult.catch  // "undefined"   <-- crashes here

Link to Reproduction

No hosted repro (this reproduces specifically under Metro/React Native bundling, which isn't something new.wagmi.sh can host). Minimal repro steps below reproduce it in any React Native + Expo + Metro project.

Steps To Reproduce

  1. In a React Native (Expo) app, install wagmi, @wagmi/core, and accounts, and configure a Tempo connector, e.g.:
    import { createConfig } from 'wagmi'
    import { webAuthn } from 'wagmi/tempo'
    
    export const config = createConfig({
      chains: [tempoModerato],
      connectors: [webAuthn({ ceremony: someCeremony })],
      transports: { [tempoModerato.id]: http() },
    })
  2. Run the app on a real device/simulator via Metro (expo run:ios / expo run:android), not a web/Node target.
  3. On startup, createConfigcreateStore → connector setup()getProvider()getAccountsModule() throws TypeError: undefined is not a function instead of resolving the accounts module (or throwing the intended, readable error).

Affected package(s)

@wagmi/core

Package version(s)

@wagmi/core@3.4.4 through the current latest @wagmi/core@3.6.3 — the code at packages/core/src/tempo/Connectors.ts's getAccountsModule is unchanged in this respect as of PR #5165 (which touched the same function for an unrelated Turbopack issue, #5144, but didn't change the .catch() chaining).

Current behavior

Chaining .catch() directly on import('accounts') throws TypeError: undefined is not a function in bundling environments (e.g. Metro/React Native, especially eager/non-lazy bundling) where the value returned by a transformed dynamic import() has .then() but no .catch() method — masking both the success path and the intended "dependency not found" error message.

Expected behavior

getAccountsModule (and any other import(...).catch(...) call sites in the Tempo connectors) should not assume the dynamic import() return value is a fully spec-compliant Promise. Wrapping in try { return await import('accounts') } catch { throw new Error('dependency "accounts" not found') } instead of chaining .catch() fixes this — await only requires .then(), which is present in all the environments observed (including Metro), and behaves identically to the current code in standard Node/browser/Vite/webpack/Turbopack environments where import() does return a real Promise.

Anything else?

Environment where this was diagnosed: Expo (React Native) app bundled by Metro, both with the default lazy bundling and with eager (lazy=false) bundling forced via a custom rewriteRequestUrl — reproduces in both, and is not specific to any particular Expo SDK/React Native version tested (confirmed across two consecutive SDK majors). Not a TypeScript type-checking issue — types report the code as correct; this is a runtime-only failure specific to how import() gets transformed by non-Node/browser bundlers.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions