From c7b4a379cf8627c79a01d61039d3e3b283477dc1 Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 16 Sep 2020 11:10:16 -0400 Subject: [PATCH] fix(runtime-core/async-component): fix error component when there are no error handlers fix #2129 --- .../__tests__/apiAsyncComponent.spec.ts | 45 +++++++++++++++++++ .../runtime-core/src/apiAsyncComponent.ts | 9 +++- packages/runtime-core/src/errorHandling.ts | 20 ++++++--- 3 files changed, 67 insertions(+), 7 deletions(-) diff --git a/packages/runtime-core/__tests__/apiAsyncComponent.spec.ts b/packages/runtime-core/__tests__/apiAsyncComponent.spec.ts index 0ba6079ab3d..af78ee7e0c9 100644 --- a/packages/runtime-core/__tests__/apiAsyncComponent.spec.ts +++ b/packages/runtime-core/__tests__/apiAsyncComponent.spec.ts @@ -206,6 +206,51 @@ describe('api: defineAsyncComponent', () => { expect(serializeInner(root)).toBe('resolved') }) + // #2129 + test('error with error component, without global handler', async () => { + let resolve: (comp: Component) => void + let reject: (e: Error) => void + const Foo = defineAsyncComponent({ + loader: () => + new Promise((_resolve, _reject) => { + resolve = _resolve as any + reject = _reject + }), + errorComponent: (props: { error: Error }) => props.error.message + }) + + const toggle = ref(true) + const root = nodeOps.createElement('div') + const app = createApp({ + render: () => (toggle.value ? h(Foo) : null) + }) + + app.mount(root) + expect(serializeInner(root)).toBe('') + + const err = new Error('errored out') + reject!(err) + await timeout() + expect(serializeInner(root)).toBe('errored out') + expect( + 'Unhandled error during execution of async component loader' + ).toHaveBeenWarned() + + toggle.value = false + await nextTick() + expect(serializeInner(root)).toBe('') + + // errored out on previous load, toggle and mock success this time + toggle.value = true + await nextTick() + expect(serializeInner(root)).toBe('') + + // should render this time + resolve!(() => 'resolved') + await timeout() + expect(serializeInner(root)).toBe('resolved') + }) + test('error with error + loading components', async () => { let resolve: (comp: Component) => void let reject: (e: Error) => void diff --git a/packages/runtime-core/src/apiAsyncComponent.ts b/packages/runtime-core/src/apiAsyncComponent.ts index 3f668a46242..a4c46867442 100644 --- a/packages/runtime-core/src/apiAsyncComponent.ts +++ b/packages/runtime-core/src/apiAsyncComponent.ts @@ -117,7 +117,12 @@ export function defineAsyncComponent< const onError = (err: Error) => { pendingRequest = null - handleError(err, instance, ErrorCodes.ASYNC_COMPONENT_LOADER) + handleError( + err, + instance, + ErrorCodes.ASYNC_COMPONENT_LOADER, + !errorComponent /* do not throw in dev if user provided error component */ + ) } // suspense-controlled or SSR. @@ -152,7 +157,7 @@ export function defineAsyncComponent< if (timeout != null) { setTimeout(() => { - if (!loaded.value) { + if (!loaded.value && !error.value) { const err = new Error( `Async component timed out after ${timeout}ms.` ) diff --git a/packages/runtime-core/src/errorHandling.ts b/packages/runtime-core/src/errorHandling.ts index a922e964e5d..b9896c58e19 100644 --- a/packages/runtime-core/src/errorHandling.ts +++ b/packages/runtime-core/src/errorHandling.ts @@ -99,7 +99,8 @@ export function callWithAsyncErrorHandling( export function handleError( err: unknown, instance: ComponentInternalInstance | null, - type: ErrorTypes + type: ErrorTypes, + throwInDev = true ) { const contextVNode = instance ? instance.vnode : null if (instance) { @@ -131,10 +132,15 @@ export function handleError( return } } - logError(err, type, contextVNode) + logError(err, type, contextVNode, throwInDev) } -function logError(err: unknown, type: ErrorTypes, contextVNode: VNode | null) { +function logError( + err: unknown, + type: ErrorTypes, + contextVNode: VNode | null, + throwInDev = true +) { if (__DEV__) { const info = ErrorTypeStrings[type] if (contextVNode) { @@ -144,8 +150,12 @@ function logError(err: unknown, type: ErrorTypes, contextVNode: VNode | null) { if (contextVNode) { popWarningContext() } - // crash in dev so it's more noticeable - throw err + // crash in dev by default so it's more noticeable + if (throwInDev) { + throw err + } else { + console.error(err) + } } else { // recover in prod to reduce the impact on end-user console.error(err)