facebook/react · error · Error

306

306

Error message

Element type is invalid. Received a promise that resolves to: ${loggedComponent}. Lazy element type must resolve to a class or function.${hint}

What it means

mountIndeterminateComponent() re-dispatches a fiber after a React.lazy() promise settles. The resolved value must be a function component, class, forwardRef, memo, or context; anything else falls through to this throw, which prints what the promise actually resolved to. The dominant cause is a dynamic import whose module does not export a component as its default export.

Source

Thrown at packages/react-reconciler/src/ReactFiberBeginWork.js:2203

  let hint = '';
  if (__DEV__) {
    if (
      // $FlowFixMe[invalid-compare]
      Component !== null &&
      typeof Component === 'object' &&
      // $FlowFixMe[invalid-compare]
      Component.$$typeof === REACT_LAZY_TYPE
    ) {
      hint = ' Did you wrap a component in React.lazy() more than once?';
    }
  }

  const loggedComponent = getComponentNameFromType(Component) || Component;

  // This message intentionally doesn't mention ForwardRef or MemoComponent
  // because the fact that it's a separate type of work is an
  // implementation detail.
  throw new Error(
    `Element type is invalid. Received a promise that resolves to: ${loggedComponent}. ` +
      `Lazy element type must resolve to a class or function.${hint}`,
  );
}

function mountIncompleteClassComponent(
  _current: null | Fiber,
  workInProgress: Fiber,
  Component: any,
  nextProps: any,
  renderLanes: Lanes,
) {
  resetSuspendedCurrentOnMountInLegacyMode(_current, workInProgress);

  // Promote the fiber to a class and try rendering again.
  workInProgress.tag = ClassComponent;

  // The rest of this function is a fork of `updateClassComponent`

View on GitHub (pinned to eafeac097b)

Solutions

  1. Open the module named in the error and make its default export a function or class component
  2. For named-only exports, map it: lazy(() => import('./Mod').then(m => ({default: m.NamedComp})))
  3. Remove nested lazy() wrappers - pass the import() directly to React.lazy
  4. Fix test mocks to resolve to {default: () => <div />}

Example fix

// before - Mod has no default export
const Panel = lazy(() => import('./Mod'));

// after - add a default export
export default function Mod() { /* ... */ }

// or map a named export
const Panel = lazy(() => import('./Mod').then(m => ({default: m.NamedPanel})));
Defensive patterns

Strategy: type-guard

Validate before calling

const loadPanel = async () => {
  const mod = await import('./Mod');
  if (!isComponentType(mod.default)) {
    throw new Error(`./Mod default export is ${typeof mod.default}, expected a component`);
  }
  return {default: mod.default};
};
const Panel = lazy(loadPanel);

Type guard

function isComponentType(x) {
  if (typeof x === 'function') return true;
  return typeof x === 'object' && x !== null && typeof x.$$typeof === 'symbol';
}

Try / catch

Wrap lazy components in an ErrorBoundary with a fallback UI; reset the boundary on route change so a fixed deployment's chunk is loaded fresh instead of a cached rejected promise.

Prevention

When it happens

Trigger: lazy(() => import('./Mod')) where './Mod' has only named exports; the default export is a plain object, constant, or hook; re-exporting one lazy component through another lazy() (React appends the hint 'Did you wrap a component in React.lazy() more than once?'); passing a non-component module (JSON, CSS) to lazy().

Common situations: Refactoring to named exports and forgetting the lazy wrapper; barrel files re-exporting through React.lazy; default-exporting a config object where a component was expected; test mocks resolving to {default: {}}; bundler default-export interop changes.

Related errors


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