diff --git a/packages/server-renderer/__tests__/render.spec.ts b/packages/server-renderer/__tests__/render.spec.ts index f18001a0ae4..c5076534266 100644 --- a/packages/server-renderer/__tests__/render.spec.ts +++ b/packages/server-renderer/__tests__/render.spec.ts @@ -851,6 +851,49 @@ function testRender(type: string, render: typeof renderToString) { expect(fn2).toBeCalledWith('async child error') }) + test('error handling w/ async setup, multiple component instances and template rendering error', async () => { + const fn = vi.fn() + const fn2 = vi.fn() + + const asyncChildren = defineComponent({ + async setup() {}, + template: `
asyncChildren{{notDefined.value}}
` + }) + const app = createApp({ + name: 'App', + components: { + asyncChildren + }, + template: `
`, + errorCaptured(error) { + fn(error) + } + }) + + app.config.errorHandler = error => { + fn2(error) + } + + let caughtError: any = null + try { + await renderToString(app) + } catch (error) { + caughtError = error + } + + expect(caughtError).not.toBe(null) + expect(caughtError.message).toBe( + "Cannot read properties of undefined (reading 'value')" + ) + + expect(fn).toHaveBeenCalledTimes(0) + expect(fn2).toHaveBeenCalledTimes(0) + + expect( + `Property "notDefined" was accessed during render but is not defined on instance` + ).toHaveBeenWarnedTimes(2) + }) + // https://github.com/vuejs/core/issues/3322 test('effect onInvalidate does not error', async () => { const noop = () => {} diff --git a/packages/server-renderer/src/render.ts b/packages/server-renderer/src/render.ts index a1f327b4320..a82ce8aaf02 100644 --- a/packages/server-renderer/src/render.ts +++ b/packages/server-renderer/src/render.ts @@ -107,7 +107,13 @@ export function renderComponentVNode( // Note: error display is already done by the wrapped lifecycle hook function. .catch(() => {}) } - return p.then(() => renderComponentSubTree(instance, slotScopeId)) + const renderPromise = p.then(() => + renderComponentSubTree(instance, slotScopeId) + ) + // Note: error display is already done by the wrapped lifecycle hook function. But we must add a catch here to avoid unhandled promise rejection. + renderPromise.catch(() => {}) + + return renderPromise } else { return renderComponentSubTree(instance, slotScopeId) }