Repository: vuejs/vue
Pull request: fix: avoid possible infinite loop by accessing observables in error handler (fix vuex#1505)

fix vuejs/vuex#1505

<!--
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)

- [x] Bugfix
- [ ] 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

If adding a **new feature**, the PR's description includes:
- [ ] A convincing reason for adding this feature (to avoid wasting your time, it's best to open a suggestion issue first and wait for approval before working on it)

**Other information:**

The original issue is https://github.com/vuejs/vuex/issues/1505
When we touch some observables in `errorCaptured` hook, it is registered as dependency of the rendered component. Then the infinite rendering loop happens if we update the value inside the hook.

Updated reproduction which does not have Vuex store is here. https://codesandbox.io/s/ypvy7zn9xx

Changed files:
filename: src/core/util/error.js
patch: @@ -4,25 +4,33 @@ import config from '../config'
 import { warn } from './debug'
 import { inBrowser, inWeex } from './env'
 import { isPromise } from 'shared/util'
+import { pushTarget, popTarget } from '../observer/dep'
 
 export function handleError (err: Error, vm: any, info: string) {
-  if (vm) {
-    let cur = vm
-    while ((cur = cur.$parent)) {
-      const hooks = cur.$options.errorCaptured
-      if (hooks) {
-        for (let i = 0; i < hooks.length; i++) {
-          try {
-            const capture = hooks[i].call(cur, err, vm, info) === false
-            if (capture) return
-          } catch (e) {
-            globalHandleError(e, cur, 'errorCaptured hook')
+  // Deactivate deps tracking while processing error handler to avoid possible infinite rendering.
+  // See: https://github.com/vuejs/vuex/issues/1505
+  pushTarget()
+  try {
+    if (vm) {
+      let cur = vm
+      while ((cur = cur.$parent)) {
+        const hooks = cur.$options.errorCaptured
+        if (hooks) {
+          for (let i = 0; i < hooks.length; i++) {
+            try {
+              const capture = hooks[i].call(cur, err, vm, info) === false
+              if (capture) return
+            } catch (e) {
+              globalHandleError(e, cur, 'errorCaptured hook')
+            }
           }
         }
       }
     }
+    globalHandleError(err, vm, info)
+  } finally {
+    popTarget()
   }
-  globalHandleError(err, vm, info)
 }
 
 export function invokeWithErrorHandling (

filename: test/unit/features/options/errorCaptured.spec.js
patch: @@ -209,4 +209,42 @@ describe('Options errorCaptured', () => {
 
     expect(calls).toEqual([1, 2, 3])
   })
+
+  // ref: https://github.com/vuejs/vuex/issues/1505
+  it('should not add watchers to render deps if they are referred from errorCaptured callback', done => {
+    const store = new Vue({
+      data: {
+        errors: []
+      }
+    })
+
+    const Child = {
+      computed: {
+        test() {
+          throw new Error('render error')
+        }
+      },
+
+      render(h) {
+        return h('div', {
+          attrs: {
+            'data-test': this.test
+          }
+        })
+      }
+    }
+
+    new Vue({
+      errorCaptured(error) {
+        store.errors.push(error)
+      },
+      render: h => h(Child)
+    }).$mount()
+
+    // Ensure not to trigger infinite loop
+    waitForUpdate(() => {
+      expect(store.errors.length).toBe(1)
+      expect(store.errors[0]).toEqual(new Error('render error'))
+    }).then(done)
+  })
 })