diff --git a/examples/index.html b/examples/index.html
index c0ee5bc16..f98b9e7d1 100644
--- a/examples/index.html
+++ b/examples/index.html
@@ -18,6 +18,7 @@
Vue Router Examples
Route Props
Route Alias
Route Params
+ Router errors
Transitions
Data Fetching
Navigation Guards
diff --git a/examples/router-errors/app.js b/examples/router-errors/app.js
new file mode 100644
index 000000000..749d36463
--- /dev/null
+++ b/examples/router-errors/app.js
@@ -0,0 +1,53 @@
+import Vue from 'vue'
+import VueRouter from 'vue-router'
+
+const component = {
+ template: `
+
+ {{ $route.fullPath }}
+
+ `
+}
+
+Vue.use(VueRouter)
+
+const router = new VueRouter({
+ routes: [
+ { path: '/', component }, { path: '/foo', component }
+ ]
+})
+
+router.beforeEach((to, from, next) => {
+ console.log('from', from.fullPath)
+ console.log('going to', to.fullPath)
+ if (to.query.wait) {
+ setTimeout(() => next(), 100)
+ } else if (to.query.redirect) {
+ next(to.query.redirect)
+ } else if (to.query.abort) {
+ next(false)
+ } else {
+ next()
+ }
+})
+
+new Vue({
+ el: '#app',
+ router
+})
+
+// 4 NAVIGATION ERROR CASES :
+
+// navigation duplicated
+// router.push('/foo')
+// router.push('/foo')
+
+// navigation cancelled
+// router.push('/foo?wait=y')
+// router.push('/')
+
+// navigation redirected
+// router.push('/foo?redirect=/')
+
+// navigation aborted
+// router.push('/foo?abort=y')
diff --git a/examples/router-errors/index.html b/examples/router-errors/index.html
new file mode 100644
index 000000000..12011560b
--- /dev/null
+++ b/examples/router-errors/index.html
@@ -0,0 +1,8 @@
+
+
+ /
+ /foo
+
+
+
+
diff --git a/src/history/abstract.js b/src/history/abstract.js
index 7b6bdf2a8..9437182e7 100644
--- a/src/history/abstract.js
+++ b/src/history/abstract.js
@@ -2,8 +2,8 @@
import type Router from '../index'
import { History } from './base'
-import { NavigationDuplicated } from './errors'
-import { isExtendedError } from '../util/warn'
+import { isRouterError } from '../util/warn'
+import { NavigationFailureType } from './errors'
export class AbstractHistory extends History {
index: number
@@ -51,7 +51,7 @@ export class AbstractHistory extends History {
this.updateRoute(route)
},
err => {
- if (isExtendedError(NavigationDuplicated, err)) {
+ if (isRouterError(err, NavigationFailureType.NAVIGATION_DUPLICATED)) {
this.index = targetIndex
}
}
diff --git a/src/history/base.js b/src/history/base.js
index 2b12e1967..cf79e2846 100644
--- a/src/history/base.js
+++ b/src/history/base.js
@@ -4,14 +4,20 @@ import { _Vue } from '../install'
import type Router from '../index'
import { inBrowser } from '../util/dom'
import { runQueue } from '../util/async'
-import { warn, isError, isExtendedError } from '../util/warn'
+import { warn, isError, isRouterError } from '../util/warn'
import { START, isSameRoute } from '../util/route'
import {
flatten,
flatMapComponents,
resolveAsyncComponents
} from '../util/resolve-components'
-import { NavigationDuplicated } from './errors'
+import {
+ createNavigationDuplicatedError,
+ createNavigationCancelledError,
+ createNavigationRedirectedError,
+ createNavigationAbortedError,
+ NavigationFailureType
+} from './errors'
export class History {
router: Router
@@ -104,7 +110,7 @@ export class History {
// When the user navigates through history through back/forward buttons
// we do not want to throw the error. We only throw it if directly calling
// push/replace. That's why it's not included in isError
- if (!isExtendedError(NavigationDuplicated, err) && isError(err)) {
+ if (!isRouterError(err, NavigationFailureType.NAVIGATION_DUPLICATED) && isError(err)) {
if (this.errorCbs.length) {
this.errorCbs.forEach(cb => {
cb(err)
@@ -122,7 +128,7 @@ export class History {
route.matched.length === current.matched.length
) {
this.ensureURL()
- return abort(new NavigationDuplicated(route))
+ return abort(createNavigationDuplicatedError(current, route))
}
const { updated, deactivated, activated } = resolveQueue(
@@ -146,12 +152,15 @@ export class History {
this.pending = route
const iterator = (hook: NavigationGuard, next) => {
if (this.pending !== route) {
- return abort()
+ return abort(createNavigationCancelledError(current, route))
}
try {
hook(route, current, (to: any) => {
- if (to === false || isError(to)) {
+ if (to === false) {
// next(false) -> abort navigation, ensure current URL
+ this.ensureURL(true)
+ abort(createNavigationAbortedError(current, route))
+ } else if (isError(to)) {
this.ensureURL(true)
abort(to)
} else if (
@@ -160,7 +169,7 @@ export class History {
(typeof to.path === 'string' || typeof to.name === 'string'))
) {
// next('/') or next({ path: '/' }) -> redirect
- abort()
+ abort(createNavigationRedirectedError(current, route))
if (typeof to === 'object' && to.replace) {
this.replace(to)
} else {
diff --git a/src/history/errors.js b/src/history/errors.js
index 1d2118c57..4fac5725a 100644
--- a/src/history/errors.js
+++ b/src/history/errors.js
@@ -1,22 +1,67 @@
-export class NavigationDuplicated extends Error {
- constructor (normalizedLocation) {
- super()
- this.name = this._name = 'NavigationDuplicated'
- // passing the message to super() doesn't seem to work in the transpiled version
- this.message = `Navigating to current location ("${
- normalizedLocation.fullPath
- }") is not allowed`
- // add a stack property so services like Sentry can correctly display it
- Object.defineProperty(this, 'stack', {
- value: new Error().stack,
- writable: true,
- configurable: true
- })
- // we could also have used
- // Error.captureStackTrace(this, this.constructor)
- // but it only exists on node and chrome
- }
+export const NavigationFailureType = {
+ redirected: 1,
+ aborted: 2,
+ cancelled: 3,
+ duplicated: 4
+}
+
+export function createNavigationRedirectedError (from, to) {
+ return createRouterError(
+ from,
+ to,
+ NavigationFailureType.redirected,
+ `Redirected from "${from.fullPath}" to "${stringifyRoute(to)}" via a navigation guard.`
+ )
+}
+
+export function createNavigationDuplicatedError (from, to) {
+ return createRouterError(
+ from,
+ to,
+ NavigationFailureType.duplicated,
+ `Avoided redundant navigation to current location: "${from.fullPath}".`
+ )
+}
+
+export function createNavigationCancelledError (from, to) {
+ return createRouterError(
+ from,
+ to,
+ NavigationFailureType.cancelled,
+ `Navigation cancelled from "${from.fullPath}" to "${to.fullPath}" with a new navigation.`
+ )
}
-// support IE9
-NavigationDuplicated._name = 'NavigationDuplicated'
+export function createNavigationAbortedError (from, to) {
+ return createRouterError(
+ from,
+ to,
+ NavigationFailureType.aborted,
+ `Navigation aborted from "${from.fullPath}" to "${to.fullPath}" via a navigation guard.`
+ )
+}
+
+function createRouterError (from, to, type, message) {
+ const error = new Error(message)
+ error._isRouter = true
+ error.from = from
+ error.to = to
+ error.type = type
+
+ const newStack = error.stack.split('\n')
+ newStack.splice(1, 2) // remove 2 last useless calls
+ error.stack = newStack.join('\n')
+ return error
+}
+
+const propertiesToLog = ['params', 'query', 'hash']
+
+function stringifyRoute (to) {
+ if (typeof to === 'string') return to
+ if ('path' in to) return to.path
+ const location = {}
+ for (const key of propertiesToLog) {
+ if (key in to) location[key] = to[key]
+ }
+ return JSON.stringify(location, null, 2)
+}
diff --git a/src/util/warn.js b/src/util/warn.js
index 9e8982fb6..73e70caf8 100644
--- a/src/util/warn.js
+++ b/src/util/warn.js
@@ -16,10 +16,6 @@ export function isError (err: any): boolean {
return Object.prototype.toString.call(err).indexOf('Error') > -1
}
-export function isExtendedError (constructor: Function, err: any): boolean {
- return (
- err instanceof constructor ||
- // _name is to support IE9 too
- (err && (err.name === constructor.name || err._name === constructor._name))
- )
+export function isRouterError (err: any, errorType: ?string): boolean {
+ return isError(err) && err._isRouter && (errorType == null || err.type === errorType)
}
diff --git a/test/unit/specs/error-handling.spec.js b/test/unit/specs/error-handling.spec.js
index 9daf5b172..8d41bca18 100644
--- a/test/unit/specs/error-handling.spec.js
+++ b/test/unit/specs/error-handling.spec.js
@@ -1,5 +1,6 @@
import Vue from 'vue'
import VueRouter from '../../../src/index'
+import { NavigationFailureType } from '../../../src/history/errors'
Vue.use(VueRouter)
@@ -40,6 +41,56 @@ describe('error handling', () => {
})
})
+ it('NavigationDuplicated error', done => {
+ const router = new VueRouter()
+
+ router.push('/foo')
+ router.push('/foo').catch(err => {
+ expect(err.type).toBe(NavigationFailureType.duplicated)
+ done()
+ })
+ })
+
+ it('NavigationCancelled error', done => {
+ const router = new VueRouter()
+
+ router.beforeEach((to, from, next) => {
+ setTimeout(() => next(), 100)
+ })
+
+ router.push('/foo').catch(err => {
+ expect(err.type).toBe(NavigationFailureType.cancelled)
+ done()
+ })
+ router.push('/')
+ })
+
+ it('NavigationRedirected error', done => {
+ const router = new VueRouter()
+
+ router.beforeEach((to, from, next) => {
+ if (to.query.redirect) {
+ next(to.query.redirect)
+ }
+ })
+
+ router.push('/foo?redirect=/').catch(err => {
+ expect(err.type).toBe(NavigationFailureType.redirected)
+ done()
+ })
+ })
+
+ it('NavigationAborted error', done => {
+ const router = new VueRouter()
+
+ router.beforeEach((to, from, next) => { next(false) })
+
+ router.push('/foo').catch(err => {
+ expect(err.type).toBe(NavigationFailureType.aborted)
+ done()
+ })
+ })
+
it('async component errors', done => {
spyOn(console, 'warn')
const err = new Error('foo')