vuejs/vue · error · Error

Invalid async component load result: ${comp}

Error message

Invalid async component load result: ${comp}

What it means

Thrown by Vue's defineAsyncComponent (src/v3/apiAsyncComponent.ts) only in __DEV__ builds. After the loader resolves and ES-module interop unwrapping runs (comp = comp.default when the module is flagged __esModule or tagged 'Module'), the result must be a Vue component — i.e. an object (options/defineComponent) or a function (functional component). If the resolved value is a non-null primitive (string, number, boolean, symbol, bigint), the loader is returning something that can never render, so the library throws rather than render a silent blank. In production builds the check is elided and the bad value is returned as-is.

Source

Thrown at src/v3/apiAsyncComponent.ts:99

          .then((comp: any) => {
            if (thisRequest !== pendingRequest && pendingRequest) {
              return pendingRequest
            }
            if (__DEV__ && !comp) {
              warn(
                `Async component loader resolved to undefined. ` +
                  `If you are using retry(), make sure to return its return value.`
              )
            }
            // interop module default
            if (
              comp &&
              (comp.__esModule || comp[Symbol.toStringTag] === 'Module')
            ) {
              comp = comp.default
            }
            if (__DEV__ && comp && !isObject(comp) && !isFunction(comp)) {
              throw new Error(`Invalid async component load result: ${comp}`)
            }
            return comp
          }))
    )
  }

  return () => {
    const component = load()

    return {
      component,
      delay,
      timeout,
      error: errorComponent,
      loading: loadingComponent
    }
  }
}

View on GitHub (pinned to 9e88707940)

Solutions

  1. Open the target module and confirm its default export is a Vue component object or function; fix the export.
  2. If the component is a named export, map it in the loader: `defineAsyncComponent(() => import('./C.vue').then(m => m.NamedComp))`.
  3. If interop is not flagging the namespace, unwrap default explicitly: `defineAsyncComponent(() => import('./C').then(m => m.default || m))`.
  4. Verify the import path resolves to a .vue file or component module (check for accidental import of a constants/util file).
  5. Reproduce in a dev build to get the throw, then inspect `comp` in a debugger at apiAsyncComponent.ts:99 to see exactly what the loader returned.

Example fix

// before — loader resolves to a primitive string
const C = defineAsyncComponent(() => import('./labels').then(m => m.TITLE))

// after — resolve to an actual component
const C = defineAsyncComponent(() => import('./TitleComponent.vue'))
// or, named component export
const C = defineAsyncComponent(() => import('./mods').then(m => m.TitleComponent))
Defensive patterns

Strategy: type-guard

Validate before calling

// Wrap any loader passed to defineAsyncComponent so a non-component
// resolution becomes a rejected promise (handled by onError / errorComponent)
// instead of throwing inside the lib.
import { defineAsyncComponent } from 'vue'

const asComponent = (m) => {
  const comp =
    m && (m.__esModule || m[Symbol.toStringTag] === 'Module') ? m.default : m
  if (comp == null) throw new Error('Async loader resolved to undefined')
  if (typeof comp !== 'object' && typeof comp !== 'function') {
    throw new Error(`Async loader resolved to non-component: ${typeof comp}`)
  }
  return comp
}

const AsyncComp = defineAsyncComponent({
  loader: () => import('./MaybeComponent.vue').then(asComponent),
  errorComponent: FallbackComp
})

Type guard

// Narrow an unknown async resolution to a Vue-component-like value.
const isVueComponentLike = (c) =>
  c != null && (typeof c === 'object' || typeof c === 'function')

// Use inside a custom loader before returning:
//   const m = await import(path)
//   const comp = m.default ?? m
//   if (!isVueComponentLike(comp)) {
//     return Promise.reject(new Error(`${path} did not export a component`))
//   }
//   return comp

Try / catch

// defineAsyncComponent already swallows loader errors via onError/errorComponent.
// Provide both so a non-component resolution surfaces in the UI instead of throwing:
defineAsyncComponent({
  loader: () => import('./Risky.vue').then(m => m.default ?? m),
  loadingComponent: LoadingSpinner,
  errorComponent: ErrorBox,
  timeout: 8000,
  onError(error, retry, fail, attempts) {
    if (attempts <= 2) retry()
    else fail()
  }
})

Prevention

When it happens

Trigger: Loader's dynamic import points at a non-component module (a constant, a JSON file, a plain string export). A loader that does `() => import('./constants').then(m => m.SOME_STRING)` returning a named string export. A loader wrapped in retry()/onError plumbing that drops the resolved value and resolves to a primitive. A module whose default export is a primitive rather than a component definition. A hand-written loader returning `() => 'MyComponent'` instead of an import.

Common situations: Refactoring a component directory and leaving an import path pointing at an index that now re-exports a constant. Migrating a CommonJS module whose module.exports was set to a string/number. Webpack chunk misconfiguration returning the module ID instead of its namespace. A Babel/swc interop edge case where __esModule is absent so the namespace object itself (a primitive field) is returned. Forgetting `default` when the component is exported as default and interop did not flag __esModule.

Related errors


AI-assisted analysis of vuejs/vue@9e88707940 (2026-08-11). Data as JSON: /api/errors/651c84cf9a6d5f63. Report an issue: GitHub.