Skip to content

TypeScript SDK: Promise.withResolvers breaks React Native (Hermes engine) #5342

@sidanet

Description

@sidanet

The TypeScript client SDK uses Promise.withResolvers() (an ES2024 API) in db_connection_impl.ts, which is not available in React Native's Hermes JavaScript engine. This causes a TypeError: undefined is not a function crash whenever a reducer or procedure is called from a React Native app on iOS or Android.

The SDK connects successfully and subscriptions work, but every reducer call crashes because the promise creation fails before the message is even sent.

Environment

  • SpacetimeDB version: 2.5.0
  • spacetimedb npm package: 2.5.0
  • Client framework: React Native (Expo SDK 56) with Hermes engine
  • Platforms affected: iOS Simulator, Android (any React Native app using Hermes)
  • Platform working fine: Web (browsers support Promise.withResolvers)

Steps to Reproduce

  1. Create a React Native (Expo) project with SpacetimeDB
  2. Connect to a SpacetimeDB module — connection succeeds
  3. Call any reducer (e.g., conn.reducers.addPlayer({ name: 'Alice' }))
  4. Observe crash: TypeError: undefined is not a function

Error & Stack Trace

TypeError: undefined is not a function
    at #callReducerWithEncodedName (localhost:8081/index…es-stable:110857:32)
    at anonymous (localhost:8081/index…es-stable:110324:52)
    at apply (native)
    at anonymous (localhost:8081/index…es-stable:103789:16)
    at handle (localhost:8081/index…es-stable:101678:17)
    at _performTransitionSideEffects (localhost:8081/index…es-stable:63205:20)
    ...

Root Cause

In crates/bindings-typescript/src/sdk/db_connection_impl.ts, four methods use Promise.withResolvers():

  • Line 1107: #callReducerWithEncodedName — called on every reducer invocation
  • Line 1142: #callReducerGeneric — fallback reducer path
  • Line 1223: #callProcedureWithEncodedName — called on every procedure invocation
  • Line 1240: #callProcedureGeneric — fallback procedure path
    Promise.withResolvers() is an ES2024 feature that is supported in modern browsers and Node.js 22+, but not in React Native's Hermes engine (the default JS engine for React Native). Hermes does not yet implement this API — Promise.withResolvers evaluates to undefined, so calling it throws TypeError: undefined is not a function.

This is notable because the SDK already contains a React Native compatibility workaround (line 241-243, the URL.toString() fix), showing awareness of the RN environment. The SDK docs also state support for "web apps, Node.js, Deno, Bun, and other JavaScript runtimes" — React Native is a major JavaScript runtime that should be covered.

Suggested Fix

Option A: Internal polyfill (minimal, zero dependencies)

Add a helper at the top of db_connection_impl.ts:

/**
 * Polyfill for Promise.withResolvers (ES2024).
 * Required for React Native's Hermes engine which does not support this API.
 */
function promiseWithResolvers<T>(): {
  promise: Promise<T>;
  resolve: (value: T | PromiseLike<T>) => void;
  reject: (reason?: any) => void;
} {
  let resolve!: (value: T | PromiseLike<T>) => void;
  let reject!: (reason?: any) => void;
  const promise = new Promise<T>((res, rej) => {
    resolve = res;
    reject = rej;
  });
  return { promise, resolve, reject };
}

Then replace the four Promise.withResolvers<T>() calls with promiseWithResolvers<T>().

This approach:

  • Has zero runtime overhead on environments that already support the API (the helper is functionally identical)
  • Requires no global mutation (unlike a Promise.withResolvers polyfill patch)
  • Is a 4-line change at each call site
  • Keeps the SDK self-contained with no new dependencies
    Option B: Global polyfill guard

Add at the top of the SDK entry point:

if (typeof Promise.withResolvers === 'undefined') {
  Promise.withResolvers = function <T>() {
    let resolve!: (value: T | PromiseLike<T>) => void;
    let reject!: (reason?: any) => void;
    const promise = new Promise<T>((res, rej) => {
      resolve = res;
      reject = rej;
    });
    return { promise, resolve, reject };
  };
}

Current Workaround

Users can add a polyfill in their app entry point before any SpacetimeDB imports:

// polyfills.ts — must be imported FIRST
if (typeof Promise.withResolvers === 'undefined') {
  Promise.withResolvers = function <T>() {
    let resolve!: (value: T | PromiseLike<T>) => void;
    let reject!: (reason?: any) => void;
    const promise = new Promise<T>((res, rej) => {
      resolve = res;
      reject = rej;
    });
    return { promise, resolve, reject };
  };
}

This works but requires every React Native developer to discover and apply this fix independently.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    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