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
- Open the target module and confirm its default export is a Vue component object or function; fix the export.
- If the component is a named export, map it in the loader: `defineAsyncComponent(() => import('./C.vue').then(m => m.NamedComp))`.
- If interop is not flagging the namespace, unwrap default explicitly: `defineAsyncComponent(() => import('./C').then(m => m.default || m))`.
- Verify the import path resolves to a .vue file or component module (check for accidental import of a constants/util file).
- 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
- Always point async loaders at .vue files or modules explicitly exported as components; avoid importing barrel/index files whose contents you do not control.
- Prefer `() => import('./X.vue')` over `() => import('./X')` so the bundler resolves the SFC unambiguously.
- Map named exports explicitly: `.then(m => m.Named)` rather than relying on default interop.
- Provide an `errorComponent` so any loader failure (including this throw in dev) is visible in the UI instead of crashing render.
- Remember this throw is dev-only — a production build will silently render nothing, so catch bad resolutions during dev/CI with a loader wrapper.
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
- [@vue/compiler-sfc] SFC contains no <script> tags.
- [@vue/compiler-sfc] <script> and <script setup> must have th
- [@vue/compiler-sfc] ${msg}\n\n${filename}\n${generateCodeFra
- \n\nVue packages version mismatch:\n\n- vue@${vueVersion}\n-
- \n\nVue packages version mismatch:\n\n- vue@${vueVersion} ($
AI-assisted analysis of vuejs/vue@9e88707940 (2026-08-11).
Data as JSON: /api/errors/651c84cf9a6d5f63.
Report an issue: GitHub.