feat(nuxt): improve error dx for users (#4539)

Co-authored-by: Pooya Parsa <pooya@pi0.io>
This commit is contained in:
Daniel Roe 2022-07-21 15:29:03 +01:00 committed by GitHub
parent 1a862526fe
commit 78618f1f21
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 143 additions and 49 deletions

View File

@ -80,13 +80,38 @@ This function will return the global Nuxt error that is being handled.
::ReadMore{link="/api/composables/use-error"}
::
### `throwError`
### `createError`
* `function throwError (err: string | Error): Error`
* `function createError (err: { cause, data, message, name, stack, statusCode, statusMessage, fatal }): Error`
You can call this function at any point on client-side, or (on server side) directly within middleware, plugins or `setup()` functions. It will trigger a full-screen error page (as above) which you can clear with `clearError`.
You can use this function to create an error object with additional metadata. It is usable in both the Vue and Nitro portions of your app, and is meant to be thrown.
::ReadMore{link="/api/utils/throw-error"}
If you throw an error created with `createError`:
* on server-side, it will trigger a full-screen error page which you can clear with `clearError`.
* on client-side, it will throw a non-fatal error for you to handle. If you need to trigger a full-screen error page, then you can do this by setting `fatal: true`.
### Example
```vue [pages/movies/[slug].vue]
<script setup>
const route = useRoute()
const { data } = await useFetch(`/api/movies/${route.params.slug}`)
if (!data.value) {
throw createError({ statusCode: 404, statusMessage: 'Page Not Found' })
}
</script>
```
### `showError`
* `function showError (err: string | Error | { statusCode, statusMessage }): Error`
You can call this function at any point on client-side, or (on server side) directly within middleware, plugins or `setup()` functions. It will trigger a full-screen error page which you can clear with `clearError`.
It is recommended instead to use `throw createError()`.
::ReadMore{link="/api/utils/show-error"}
::
### `clearError`

View File

@ -0,0 +1,44 @@
# `createError`
You can use this function to create an error object with additional metadata. It is usable in both the Vue and Nitro portions of your app, and is meant to be thrown.
**Parameters:**
* err: { cause, data, message, name, stack, statusCode, statusMessage, fatal }
## Throwing errors in your Vue app
If you throw an error created with `createError`:
* on server-side, it will trigger a full-screen error page which you can clear with `clearError`.
* on client-side, it will throw a non-fatal error for you to handle. If you need to trigger a full-screen error page, then you can do this by setting `fatal: true`.
### Example
```vue [pages/movies/[slug].vue]
<script setup>
const route = useRoute()
const { data } = await useFetch(`/api/movies/${route.params.slug}`)
if (!data.value) {
throw createError({ statusCode: 404, statusMessage: 'Page Not Found' })
}
</script>
```
## Throwing errors in API routes
You can use `createError` to trigger error handling in server API routes.
### Example
```js
export default eventHandler(() => {
throw createError({
statusCode: 404,
statusMessage: 'Page Not Found'
})
}
```
::ReadMore{link="/guide/features/error-handling"}
::

View File

@ -0,0 +1,21 @@
# `showError`
Nuxt provides a quick and simple way to show a full screen error page if needed.
Within your pages, components and plugins you can use `showError` to show an error error.
**Parameters:**
- `error`: `string | Error | Partial<{ cause, data, message, name, stack, statusCode, statusMessage }>`
```js
showError("😱 Oh no, an error has been thrown.")
showError({ statusCode: 404, statusMessage: "Page Not Found" })
```
The error is set in the state using [`useError()`](/api/composables/use-error) to create a reactive and SSR-friendly shared error state across components.
`showError` calls the `app:error` hook.
::ReadMore{link="/guide/features/error-handling"}
::

View File

@ -1,20 +0,0 @@
# `throwError`
Nuxt provides a quick and simple way to throw errors.
Within your pages, components and plugins you can use `throwError` to throw an error.
**Parameters:**
- `error`: `string | Error`
```js
throwError("😱 Oh no, an error has been thrown.")
```
The thrown error is set in the state using [`useError()`](/api/composables/use-error) to create a reactive and SSR-friendly shared error state across components.
`throwError` calls the `app:error` hook.
::ReadMore{link="/guide/features/error-handling"}
::

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { throwError } from '#app'
import { showError } from '#app'
const route = useRoute()
if ('setup' in route.query) {
throw new Error('error in setup')
@ -30,7 +30,7 @@ function triggerError () {
<NuxtLink to="/?middleware" class="n-link-base">
Middleware
</NuxtLink>
<button class="n-link-base" @click="throwError">
<button class="n-link-base" @click="showError">
Trigger fatal error
</button>
<button class="n-link-base" @click="triggerError">

View File

@ -1,5 +1,5 @@
export default defineNuxtRouteMiddleware((to) => {
if ('middleware' in to.query) {
return throwError('error in middleware')
return showError('error in middleware')
}
})

View File

@ -7,7 +7,7 @@
<script setup>
import { defineAsyncComponent, onErrorCaptured } from 'vue'
import { callWithNuxt, throwError, useError, useNuxtApp } from '#app'
import { callWithNuxt, isNuxtError, showError, useError, useNuxtApp } from '#app'
const ErrorComponent = defineAsyncComponent(() => import('#build/error-component.mjs'))
@ -24,8 +24,8 @@ if (process.dev && results && results.some(i => i && 'then' in i)) {
const error = useError()
onErrorCaptured((err, target, info) => {
nuxtApp.hooks.callHook('vue:error', err, target, info).catch(hookError => console.error('[nuxt] Error in `vue:error` hook', hookError))
if (process.server) {
callWithNuxt(nuxtApp, throwError, [err])
if (process.server || (isNuxtError(err) && (err.fatal || err.unhandled))) {
callWithNuxt(nuxtApp, showError, [err])
}
})
</script>

View File

@ -1,3 +1,4 @@
import { createError as _createError, H3Error } from 'h3'
import { useNuxtApp, useState } from '#app'
export const useError = () => {
@ -5,20 +6,31 @@ export const useError = () => {
return useState('error', () => process.server ? nuxtApp.ssrContext.error : nuxtApp.payload.error)
}
export const throwError = (_err: string | Error) => {
const nuxtApp = useNuxtApp()
const error = useError()
const err = typeof _err === 'string' ? new Error(_err) : _err
nuxtApp.callHook('app:error', err)
if (process.server) {
nuxtApp.ssrContext.error = nuxtApp.ssrContext.error || err
} else {
error.value = error.value || err
export interface NuxtError extends H3Error {}
export const showError = (_err: string | Error | Partial<NuxtError>) => {
const err = createError(_err)
err.fatal = true
try {
const nuxtApp = useNuxtApp()
nuxtApp.callHook('app:error', err)
if (process.server) {
nuxtApp.ssrContext.error = nuxtApp.ssrContext.error || err
} else {
const error = useError()
error.value = error.value || err
}
} catch {
throw err
}
return err
}
/** @deprecated Use `throw createError()` or `showError` */
export const throwError = showError
export const clearError = async (options: { redirect?: string } = {}) => {
const nuxtApp = useNuxtApp()
const error = useError()
@ -28,3 +40,11 @@ export const clearError = async (options: { redirect?: string } = {}) => {
}
error.value = null
}
export const isNuxtError = (err?: string | object): err is NuxtError => err && typeof err === 'object' && ('__nuxt_error' in err)
export const createError = (err: string | Partial<NuxtError>): NuxtError => {
const _err: NuxtError = _createError(err)
;(_err as any).__nuxt_error = true
return _err
}

View File

@ -3,7 +3,8 @@ export { useAsyncData, useLazyAsyncData, refreshNuxtData } from './asyncData'
export type { AsyncDataOptions, AsyncData } from './asyncData'
export { useHydration } from './hydrate'
export { useState } from './state'
export { clearError, throwError, useError } from './error'
export { clearError, createError, isNuxtError, throwError, showError, useError } from './error'
export type { NuxtError } from './error'
export { useFetch, useLazyFetch } from './fetch'
export type { FetchResult, UseFetchOptions } from './fetch'
export { useCookie } from './cookie'

View File

@ -3,7 +3,7 @@ import { parseURL, parseQuery, withoutBase, isEqual, joinURL } from 'ufo'
import { createError } from 'h3'
import { defineNuxtPlugin } from '..'
import { callWithNuxt } from '../nuxt'
import { clearError, navigateTo, throwError, useRuntimeConfig } from '#app'
import { clearError, navigateTo, showError, useRuntimeConfig } from '#app'
// @ts-ignore
import { globalMiddleware } from '#build/middleware'
@ -228,7 +228,7 @@ export default defineNuxtPlugin<{ route: Route, router: Router }>((nuxtApp) => {
const error = result || createError({
statusMessage: `Route navigation aborted: ${initialURL}`
})
return callWithNuxt(nuxtApp, throwError, [error])
return callWithNuxt(nuxtApp, showError, [error])
}
}
if (result || result === false) { return result }

View File

@ -43,8 +43,11 @@ export const appPreset = defineUnimportPreset({
'abortNavigation',
'addRouteMiddleware',
'throwError',
'showError',
'clearError',
'isNuxtError',
'useError',
'createError',
'defineNuxtLink'
]
})

View File

@ -24,8 +24,8 @@ export default <NitroErrorHandler> async function errorhandler (_error, event) {
event.res.statusMessage = errorObject.statusMessage
// Console output
if (errorObject.statusCode !== 404) {
console.error('[nuxt] [request error]', errorObject.message + '\n' + stack.map(l => ' ' + l.text).join(' \n'))
if ((_error as any).unhandled) {
console.error('[nuxt] [unhandled request error]', errorObject.message + '\n' + stack.map(l => ' ' + l.text).join(' \n'))
}
// JSON response

View File

@ -9,7 +9,7 @@ import {
import { createError } from 'h3'
import { withoutBase, isEqual } from 'ufo'
import NuxtPage from './page'
import { callWithNuxt, defineNuxtPlugin, useRuntimeConfig, throwError, clearError, navigateTo, useError } from '#app'
import { callWithNuxt, defineNuxtPlugin, useRuntimeConfig, showError, clearError, navigateTo, useError } from '#app'
// @ts-ignore
import routes from '#build/routes'
// @ts-ignore
@ -117,7 +117,7 @@ export default defineNuxtPlugin(async (nuxtApp) => {
await router.isReady()
} catch (error) {
// We'll catch 404s here
callWithNuxt(nuxtApp, throwError, [error])
callWithNuxt(nuxtApp, showError, [error])
}
router.beforeEach(async (to, from) => {
@ -154,7 +154,7 @@ export default defineNuxtPlugin(async (nuxtApp) => {
const error = result || createError({
statusMessage: `Route navigation aborted: ${initialURL}`
})
return callWithNuxt(nuxtApp, throwError, [error])
return callWithNuxt(nuxtApp, showError, [error])
}
}
if (result || result === false) { return result }
@ -169,7 +169,7 @@ export default defineNuxtPlugin(async (nuxtApp) => {
await callWithNuxt(nuxtApp, clearError)
}
if (to.matched.length === 0) {
callWithNuxt(nuxtApp, throwError, [createError({
callWithNuxt(nuxtApp, showError, [createError({
statusCode: 404,
statusMessage: `Page not found: ${to.fullPath}`
})])
@ -192,7 +192,7 @@ export default defineNuxtPlugin(async (nuxtApp) => {
})
} catch (error) {
// We'll catch middleware errors or deliberate exceptions here
callWithNuxt(nuxtApp, throwError, [error])
callWithNuxt(nuxtApp, showError, [error])
}
})