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

View File

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

View File

@ -7,7 +7,7 @@
<script setup> <script setup>
import { defineAsyncComponent, onErrorCaptured } from 'vue' 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')) 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() const error = useError()
onErrorCaptured((err, target, info) => { onErrorCaptured((err, target, info) => {
nuxtApp.hooks.callHook('vue:error', err, target, info).catch(hookError => console.error('[nuxt] Error in `vue:error` hook', hookError)) nuxtApp.hooks.callHook('vue:error', err, target, info).catch(hookError => console.error('[nuxt] Error in `vue:error` hook', hookError))
if (process.server) { if (process.server || (isNuxtError(err) && (err.fatal || err.unhandled))) {
callWithNuxt(nuxtApp, throwError, [err]) callWithNuxt(nuxtApp, showError, [err])
} }
}) })
</script> </script>

View File

@ -1,3 +1,4 @@
import { createError as _createError, H3Error } from 'h3'
import { useNuxtApp, useState } from '#app' import { useNuxtApp, useState } from '#app'
export const useError = () => { export const useError = () => {
@ -5,20 +6,31 @@ export const useError = () => {
return useState('error', () => process.server ? nuxtApp.ssrContext.error : nuxtApp.payload.error) return useState('error', () => process.server ? nuxtApp.ssrContext.error : nuxtApp.payload.error)
} }
export const throwError = (_err: string | Error) => { export interface NuxtError extends H3Error {}
export const showError = (_err: string | Error | Partial<NuxtError>) => {
const err = createError(_err)
err.fatal = true
try {
const nuxtApp = useNuxtApp() const nuxtApp = useNuxtApp()
const error = useError()
const err = typeof _err === 'string' ? new Error(_err) : _err
nuxtApp.callHook('app:error', err) nuxtApp.callHook('app:error', err)
if (process.server) { if (process.server) {
nuxtApp.ssrContext.error = nuxtApp.ssrContext.error || err nuxtApp.ssrContext.error = nuxtApp.ssrContext.error || err
} else { } else {
const error = useError()
error.value = error.value || err error.value = error.value || err
} }
} catch {
throw err
}
return err return err
} }
/** @deprecated Use `throw createError()` or `showError` */
export const throwError = showError
export const clearError = async (options: { redirect?: string } = {}) => { export const clearError = async (options: { redirect?: string } = {}) => {
const nuxtApp = useNuxtApp() const nuxtApp = useNuxtApp()
const error = useError() const error = useError()
@ -28,3 +40,11 @@ export const clearError = async (options: { redirect?: string } = {}) => {
} }
error.value = null 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 type { AsyncDataOptions, AsyncData } from './asyncData'
export { useHydration } from './hydrate' export { useHydration } from './hydrate'
export { useState } from './state' 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 { useFetch, useLazyFetch } from './fetch'
export type { FetchResult, UseFetchOptions } from './fetch' export type { FetchResult, UseFetchOptions } from './fetch'
export { useCookie } from './cookie' export { useCookie } from './cookie'

View File

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

View File

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

View File

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

View File

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