Repository: vuejs/vue
Pull request: handle errors thrown by directive hooks (#5314)

#5314
<!--
Please make sure to read the Pull Request Guidelines:
https://github.com/vuejs/vue/blob/dev/.github/CONTRIBUTING.md#pull-request-guidelines
-->

<!-- PULL REQUEST TEMPLATE -->
<!-- (Update "[ ]" to "[x]" to check a box) -->

**What kind of change does this PR introduce?** (check at least one)

- [ ] Bugfix
- [x] Feature
- [ ] Code style update
- [ ] Refactor
- [ ] Build-related changes
- [ ] Other, please describe:

**Does this PR introduce a breaking change?** (check one)

- [ ] Yes
- [x] No

If yes, please describe the impact and migration path for existing applications:

**The PR fulfills these requirements:**

- [x] It's submitted to the `dev` branch for v2.x (or to a previous version branch), _not_ the `master` branch
- [x] When resolving a specific issue, it's referenced in the PR's title (e.g. `fix #xxx[,#xxx]`, where "xxx" is the issue number)
- [x] All tests are passing: https://github.com/vuejs/vue/blob/dev/.github/CONTRIBUTING.md#development-setup
- [x] New/updated tests are included


Changed files:
filename: src/core/vdom/modules/directives.js
patch: @@ -1,7 +1,7 @@
 /* @flow */
 
 import { emptyNode } from 'core/vdom/patch'
-import { resolveAsset } from 'core/util/options'
+import { resolveAsset, handleError } from 'core/util/index'
 import { mergeVNodeHook } from 'core/vdom/helpers/index'
 
 export default {
@@ -107,6 +107,10 @@ function getRawDirName (dir: VNodeDirective): string {
 function callHook (dir, hook, vnode, oldVnode, isDestroy) {
   const fn = dir.def && dir.def[hook]
   if (fn) {
-    fn(vnode.elm, dir, vnode, oldVnode, isDestroy)
+    try {
+      fn(vnode.elm, dir, vnode, oldVnode, isDestroy)
+    } catch (e) {
+      handleError(e, vnode.context, `directive ${dir.name} ${hook} hook`)
+    }
   }
 }

filename: test/unit/features/error-handling.spec.js
patch: @@ -11,7 +11,8 @@ describe('Error handling', () => {
     ['render', 'render function'],
     ['beforeCreate', 'beforeCreate hook'],
     ['created', 'created hook'],
-    ['beforeMount', 'beforeMount hook']
+    ['beforeMount', 'beforeMount hook'],
+    ['directive bind', 'directive foo bind hook']
   ].forEach(([type, description]) => {
     it(`should recover from errors in ${type}`, done => {
       const vm = createTestInstance(components[type])
@@ -32,7 +33,8 @@ describe('Error handling', () => {
   // error in beforeUpdate/updated should affect neither child nor parent
   ;[
     ['beforeUpdate', 'beforeUpdate hook'],
-    ['updated', 'updated hook']
+    ['updated', 'updated hook'],
+    ['directive update', 'directive foo update hook']
   ].forEach(([type, description]) => {
     it(`should recover from errors in ${type} hook`, done => {
       const vm = createTestInstance(components[type])
@@ -45,7 +47,8 @@ describe('Error handling', () => {
 
   ;[
     ['beforeDestroy', 'beforeDestroy hook'],
-    ['destroyed', 'destroyed hook']
+    ['destroyed', 'destroyed hook'],
+    ['directive unbind', 'directive foo unbind hook']
   ].forEach(([type, description]) => {
     it(`should recover from errors in ${type} hook`, done => {
       const vm = createTestInstance(components[type])
@@ -173,6 +176,22 @@ function createErrorTestComponents () {
     }
   })
 
+  // directive hooks errors
+  ;['bind', 'update', 'unbind'].forEach(hook => {
+    const key = 'directive ' + hook
+    const dirComp = components[key] = {
+      props: ['n'],
+      template: `<div v-foo="n">{{ n }}</div>`
+    }
+    const dirFoo = {}
+    dirFoo[hook] = function () {
+      throw new Error(key)
+    }
+    dirComp.directives = {
+      foo: dirFoo
+    }
+  })
+
   // user watcher
   components.userWatcherGetter = {
     props: ['n'],