diff --git a/docs/content/2.guide/2.features/7.error-handling.md b/docs/content/2.guide/2.features/7.error-handling.md index e527e402a45..c514237ffde 100644 --- a/docs/content/2.guide/2.features/7.error-handling.md +++ b/docs/content/2.guide/2.features/7.error-handling.md @@ -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] + +``` + +### `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` diff --git a/docs/content/3.api/3.utils/create-error.md b/docs/content/3.api/3.utils/create-error.md new file mode 100644 index 00000000000..480b1bf7fca --- /dev/null +++ b/docs/content/3.api/3.utils/create-error.md @@ -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] + +``` + +## 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"} +:: diff --git a/docs/content/3.api/3.utils/show-error.md b/docs/content/3.api/3.utils/show-error.md new file mode 100644 index 00000000000..ff128c9b0bd --- /dev/null +++ b/docs/content/3.api/3.utils/show-error.md @@ -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"} +:: diff --git a/docs/content/3.api/3.utils/throw-error.md b/docs/content/3.api/3.utils/throw-error.md deleted file mode 100644 index 7a9306def28..00000000000 --- a/docs/content/3.api/3.utils/throw-error.md +++ /dev/null @@ -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"} -:: diff --git a/examples/app/error-handling/app.vue b/examples/app/error-handling/app.vue index 66f9ee9c715..bbd937d8ded 100644 --- a/examples/app/error-handling/app.vue +++ b/examples/app/error-handling/app.vue @@ -1,5 +1,5 @@ diff --git a/packages/nuxt/src/app/composables/error.ts b/packages/nuxt/src/app/composables/error.ts index cdee4541c7b..e50c8b72500 100644 --- a/packages/nuxt/src/app/composables/error.ts +++ b/packages/nuxt/src/app/composables/error.ts @@ -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) => { + 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 => { + const _err: NuxtError = _createError(err) + ;(_err as any).__nuxt_error = true + return _err +} diff --git a/packages/nuxt/src/app/composables/index.ts b/packages/nuxt/src/app/composables/index.ts index 2780a248ca6..cb3eb864379 100644 --- a/packages/nuxt/src/app/composables/index.ts +++ b/packages/nuxt/src/app/composables/index.ts @@ -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' diff --git a/packages/nuxt/src/app/plugins/router.ts b/packages/nuxt/src/app/plugins/router.ts index 50e5d6bae1e..bde980db0c7 100644 --- a/packages/nuxt/src/app/plugins/router.ts +++ b/packages/nuxt/src/app/plugins/router.ts @@ -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 } diff --git a/packages/nuxt/src/auto-imports/presets.ts b/packages/nuxt/src/auto-imports/presets.ts index cf2fd30dea5..f2640359034 100644 --- a/packages/nuxt/src/auto-imports/presets.ts +++ b/packages/nuxt/src/auto-imports/presets.ts @@ -43,8 +43,11 @@ export const appPreset = defineUnimportPreset({ 'abortNavigation', 'addRouteMiddleware', 'throwError', + 'showError', 'clearError', + 'isNuxtError', 'useError', + 'createError', 'defineNuxtLink' ] }) diff --git a/packages/nuxt/src/core/runtime/nitro/error.ts b/packages/nuxt/src/core/runtime/nitro/error.ts index 381832fb500..a34db5c670d 100644 --- a/packages/nuxt/src/core/runtime/nitro/error.ts +++ b/packages/nuxt/src/core/runtime/nitro/error.ts @@ -24,8 +24,8 @@ export default 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 diff --git a/packages/nuxt/src/pages/runtime/router.ts b/packages/nuxt/src/pages/runtime/router.ts index 824dddeeaea..2ab21f2a261 100644 --- a/packages/nuxt/src/pages/runtime/router.ts +++ b/packages/nuxt/src/pages/runtime/router.ts @@ -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]) } })