From b3cd9bc3940eb1e01da7081450929557d9c1651e Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 5 Oct 2017 14:59:38 -0400 Subject: [PATCH] feat: add catchError option also propagate error thrown in renderError() to global handler --- src/core/instance/render.js | 15 ++- src/core/util/error.js | 22 +++- test/unit/features/error-handling.spec.js | 4 +- test/unit/features/options/catchError.spec.js | 121 ++++++++++++++++++ .../unit/features/options/renderError.spec.js | 14 ++ 5 files changed, 167 insertions(+), 9 deletions(-) create mode 100644 test/unit/features/options/catchError.spec.js diff --git a/src/core/instance/render.js b/src/core/instance/render.js index 15bc9a30192..b492316a0df 100644 --- a/src/core/instance/render.js +++ b/src/core/instance/render.js @@ -101,14 +101,21 @@ export function renderMixin (Vue: Class) { try { vnode = render.call(vm._renderProxy, vm.$createElement) } catch (e) { - handleError(e, vm, `render function`) + handleError(e, vm, `render`) // return error render result, // or previous vnode to prevent render error causing blank component /* istanbul ignore else */ if (process.env.NODE_ENV !== 'production') { - vnode = vm.$options.renderError - ? vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e) - : vm._vnode + if (vm.$options.renderError) { + try { + vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e) + } catch (e) { + handleError(e, vm, `renderError`) + vnode = vm._vnode + } + } else { + vnode = vm._vnode + } } else { vnode = vm._vnode } diff --git a/src/core/util/error.js b/src/core/util/error.js index b961986a585..9b87f89e465 100644 --- a/src/core/util/error.js +++ b/src/core/util/error.js @@ -5,12 +5,28 @@ import { warn } from './debug' import { inBrowser } from './env' export function handleError (err: Error, vm: any, info: string) { + if (vm) { + let cur = vm + while ((cur = cur.$parent)) { + if (cur.$options.catchError) { + try { + const propagate = cur.$options.catchError.call(cur, err, vm, info) + if (!propagate) return + } catch (e) { + globalHandleError(e, cur, 'catchError') + } + } + } + } + globalHandleError(err, vm, info) +} + +function globalHandleError (err, vm, info) { if (config.errorHandler) { try { - config.errorHandler.call(null, err, vm, info) - return + return config.errorHandler.call(null, err, vm, info) } catch (e) { - logError(e, null, 'errorHandler') + logError(e, null, 'config.errorHandler') } } logError(err, vm, info) diff --git a/test/unit/features/error-handling.spec.js b/test/unit/features/error-handling.spec.js index 5bf05fdb120..217bb72721a 100644 --- a/test/unit/features/error-handling.spec.js +++ b/test/unit/features/error-handling.spec.js @@ -7,7 +7,7 @@ describe('Error handling', () => { // break parent component ;[ ['data', 'data()'], - ['render', 'render function'], + ['render', 'render'], ['beforeCreate', 'beforeCreate hook'], ['created', 'created hook'], ['beforeMount', 'beforeMount hook'], @@ -99,7 +99,7 @@ describe('Error handling', () => { const args = spy.calls.argsFor(0) expect(args[0].toString()).toContain('Error: render') // error expect(args[1]).toBe(vm.$refs.child) // vm - expect(args[2]).toContain('render function') // description + expect(args[2]).toContain('render') // description assertRootInstanceActive(vm).then(() => { Vue.config.errorHandler = null diff --git a/test/unit/features/options/catchError.spec.js b/test/unit/features/options/catchError.spec.js new file mode 100644 index 00000000000..27a12145fce --- /dev/null +++ b/test/unit/features/options/catchError.spec.js @@ -0,0 +1,121 @@ +import Vue from 'vue' + +describe('Options catchError', () => { + let globalSpy + + beforeEach(() => { + globalSpy = Vue.config.errorHandler = jasmine.createSpy() + }) + + afterEach(() => { + Vue.config.errorHandler = null + }) + + it('should capture error from child component', () => { + const spy = jasmine.createSpy() + + let child + let err + const Child = { + created () { + child = this + err = new Error('child') + throw err + }, + render () {} + } + + new Vue({ + catchError: spy, + render: h => h(Child) + }).$mount() + + expect(spy).toHaveBeenCalledWith(err, child, 'created hook') + // should not propagate by default + expect(globalSpy).not.toHaveBeenCalled() + }) + + it('should be able to render the error in itself', done => { + let child + const Child = { + created () { + child = this + throw new Error('error from child') + }, + render () {} + } + + const vm = new Vue({ + data: { + error: null + }, + catchError (e, vm, info) { + expect(vm).toBe(child) + this.error = e.toString() + ' in ' + info + }, + render (h) { + if (this.error) { + return h('pre', this.error) + } + return h(Child) + } + }).$mount() + + waitForUpdate(() => { + expect(vm.$el.textContent).toContain('error from child') + expect(vm.$el.textContent).toContain('in created hook') + }).then(done) + }) + + it('should propagate to global handler when returning true', () => { + const spy = jasmine.createSpy() + + let child + let err + const Child = { + created () { + child = this + err = new Error('child') + throw err + }, + render () {} + } + + new Vue({ + catchError (err, vm, info) { + spy(err, vm, info) + return true + }, + render: h => h(Child, {}) + }).$mount() + + expect(spy).toHaveBeenCalledWith(err, child, 'created hook') + // should propagate + expect(globalSpy).toHaveBeenCalledWith(err, child, 'created hook') + }) + + it('should propagate to global handler if itself throws error', () => { + let child + let err + const Child = { + created () { + child = this + err = new Error('child') + throw err + }, + render () {} + } + + let err2 + const vm = new Vue({ + catchError () { + err2 = new Error('foo') + throw err2 + }, + render: h => h(Child, {}) + }).$mount() + + expect(globalSpy).toHaveBeenCalledWith(err, child, 'created hook') + expect(globalSpy).toHaveBeenCalledWith(err2, vm, 'catchError') + }) +}) diff --git a/test/unit/features/options/renderError.spec.js b/test/unit/features/options/renderError.spec.js index 36b263b8565..20a0aa749a2 100644 --- a/test/unit/features/options/renderError.spec.js +++ b/test/unit/features/options/renderError.spec.js @@ -25,4 +25,18 @@ describe('Options renderError', () => { Vue.config.errorHandler = null }).then(done) }) + + it('should pass on errors in renderError to global handler', () => { + const spy = Vue.config.errorHandler = jasmine.createSpy() + const err = new Error('renderError') + const vm = new Vue({ + render () { + throw new Error('render') + }, + renderError () { + throw err + } + }).$mount() + expect(spy).toHaveBeenCalledWith(err, vm, 'renderError') + }) })