facebook/react · error · Error

Expected source to have been loaded into a string.

Error message

Expected source to have been loaded into a string.

What it means

The loader's load hook requires that defaultLoad's result for format 'module' carry a string source, because transformModuleIfNeeded then parses the source for 'use client'/'use server' directives and rewrites client-reference imports. Node legally hands loaders ArrayBuffer or Uint8Array sources, and any non-string reaching this check throws.

Source

Thrown at packages/react-server-dom-esm/src/ReactFlightESMNodeLoader.js:794

      context.url,
      (url: string, ctx: LoadContext, defaultLoad: LoadFunction) => {
        return loadClientImport(url, defaultTransformSource);
      },
    );
    return {source: newSrc};
  }
  return transformed;
}

export async function load(
  url: string,
  context: LoadContext,
  defaultLoad: LoadFunction,
): Promise<{format: string, shortCircuit?: boolean, source: Source}> {
  const result = await defaultLoad(url, context, defaultLoad);
  if (result.format === 'module') {
    if (typeof result.source !== 'string') {
      throw new Error('Expected source to have been loaded into a string.');
    }
    const newSrc = await transformModuleIfNeeded(
      result.source,
      url,
      defaultLoad,
    );
    return {format: 'module', source: newSrc};
  }
  return result;
}

View on GitHub (pinned to eafeac097b)

Solutions

  1. Fix the earlier loader so JS module sources are returned as utf-8 strings before React's load hook sees them
  2. Add an adapter load hook that converts binary module sources with new TextDecoder().decode(source)
  3. Avoid importing modules from schemes that produce binary sources

Example fix

// before — chained loader returns a Buffer
export async function load(url, context, dflt) {
  const r = await dflt(url, context, dflt);
  return r; // r.source is a Buffer for .js files
}

// after — decode module sources to strings
export async function load(url, context, dflt) {
  const r = await dflt(url, context, dflt);
  if (r.format === 'module' && typeof r.source !== 'string') {
    return {...r, source: new TextDecoder().decode(r.source)};
  }
  return r;
}
Defensive patterns

Strategy: validation

Validate before calling

// adapter load hook placed in front of React's loader
export async function load(url, context, dflt) {
  const r = await dflt(url, context, dflt);
  if (r.format === 'module' && typeof r.source !== 'string') {
    return {...r, source: new TextDecoder().decode(r.source)};
  }
  return r;
}

Type guard

export function isStringModuleSource(result) {
  return result.format !== 'module' || typeof result.source === 'string';
}

Try / catch

try {
  await import(url);
} catch (e) {
  if (/loaded into a string/.test(e.message)) {
    console.error('Binary module source for', url, '— decode it in a preceding load hook');
  }
  throw e;
}

Prevention

When it happens

Trigger: defaultLoad (or an earlier loader in the chain) returns {format: 'module', source: ArrayBuffer|Uint8Array|Buffer} — for example a chained loader returning Buffers, or a URL scheme that Node loads as binary while still reporting format 'module'.

Common situations: Chained loaders (asset/WASM/CSS loaders returning buffers); --experimental-network-imports; Node upgrades that change the default source representation for some schemes.

Related errors


AI-assisted analysis of facebook/react@eafeac097b (2026-08-21). Data as JSON: /api/errors/fe8b95a042e33c2f. Report an issue: GitHub.