diff --git a/packages/runtime-core/__tests__/components/Suspense.spec.ts b/packages/runtime-core/__tests__/components/Suspense.spec.ts
index 421bc0a8e..b206dced4 100644
--- a/packages/runtime-core/__tests__/components/Suspense.spec.ts
+++ b/packages/runtime-core/__tests__/components/Suspense.spec.ts
@@ -709,7 +709,7 @@ describe('Suspense', () => {
{{ errorMessage }}
fallback
@@ -1232,4 +1232,25 @@ describe('Suspense', () => {
await nextTick()
expect(serializeInner(root)).toBe(`parent
`)
})
+
+ test('warn if using async setup when not in a Suspense boundary', () => {
+ const Child = {
+ name: 'Child',
+ async setup() {
+ return () => h('div', 'child')
+ }
+ }
+ const Parent = {
+ setup() {
+ return () => h('div', [h(Child)])
+ }
+ }
+
+ const root = nodeOps.createElement('div')
+ render(h(Parent), root)
+
+ expect(
+ `A component with async setup() must be nested in a `
+ ).toHaveBeenWarned()
+ })
})
diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts
index 9b2f35c97..53048b9a2 100644
--- a/packages/runtime-core/src/component.ts
+++ b/packages/runtime-core/src/component.ts
@@ -654,7 +654,6 @@ function setupStatefulComponent(
if (isPromise(setupResult)) {
setupResult.then(unsetCurrentInstance, unsetCurrentInstance)
-
if (isSSR) {
// return the promise so server-renderer can wait on it
return setupResult
@@ -668,6 +667,15 @@ function setupStatefulComponent(
// async setup returned Promise.
// bail here and wait for re-entry.
instance.asyncDep = setupResult
+ if (__DEV__ && !instance.suspense) {
+ const name = Component.name ?? 'Anonymous'
+ warn(
+ `Component <${name}>: setup function returned a promise, but no ` +
+ ` boundary was found in the parent component tree. ` +
+ `A component with async setup() must be nested in a ` +
+ `in order to be rendered.`
+ )
+ }
} else if (__DEV__) {
warn(
`setup() returned a Promise, but the version of Vue you are using ` +