Skip to content
This repository was archived by the owner on Apr 6, 2023. It is now read-only.

Commit 78618f1

Browse files
danielroepi0
andauthored
feat(nuxt): improve error dx for users (#4539)
Co-authored-by: Pooya Parsa <[email protected]>
1 parent 1a86252 commit 78618f1

File tree

13 files changed

+143
-49
lines changed

13 files changed

+143
-49
lines changed

docs/content/2.guide/2.features/7.error-handling.md

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,13 +80,38 @@ This function will return the global Nuxt error that is being handled.
8080
::ReadMore{link="/api/composables/use-error"}
8181
::
8282

83-
### `throwError`
83+
### `createError`
8484

85-
* `function throwError (err: string | Error): Error`
85+
* `function createError (err: { cause, data, message, name, stack, statusCode, statusMessage, fatal }): Error`
8686

87-
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`.
87+
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.
8888

89-
::ReadMore{link="/api/utils/throw-error"}
89+
If you throw an error created with `createError`:
90+
91+
* on server-side, it will trigger a full-screen error page which you can clear with `clearError`.
92+
* 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`.
93+
94+
### Example
95+
96+
```vue [pages/movies/[slug].vue]
97+
<script setup>
98+
const route = useRoute()
99+
const { data } = await useFetch(`/api/movies/${route.params.slug}`)
100+
if (!data.value) {
101+
throw createError({ statusCode: 404, statusMessage: 'Page Not Found' })
102+
}
103+
</script>
104+
```
105+
106+
### `showError`
107+
108+
* `function showError (err: string | Error | { statusCode, statusMessage }): Error`
109+
110+
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`.
111+
112+
It is recommended instead to use `throw createError()`.
113+
114+
::ReadMore{link="/api/utils/show-error"}
90115
::
91116

92117
### `clearError`
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# `createError`
2+
3+
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.
4+
5+
**Parameters:**
6+
7+
* err: { cause, data, message, name, stack, statusCode, statusMessage, fatal }
8+
9+
## Throwing errors in your Vue app
10+
11+
If you throw an error created with `createError`:
12+
13+
* on server-side, it will trigger a full-screen error page which you can clear with `clearError`.
14+
* 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`.
15+
16+
### Example
17+
18+
```vue [pages/movies/[slug].vue]
19+
<script setup>
20+
const route = useRoute()
21+
const { data } = await useFetch(`/api/movies/${route.params.slug}`)
22+
if (!data.value) {
23+
throw createError({ statusCode: 404, statusMessage: 'Page Not Found' })
24+
}
25+
</script>
26+
```
27+
28+
## Throwing errors in API routes
29+
30+
You can use `createError` to trigger error handling in server API routes.
31+
32+
### Example
33+
34+
```js
35+
export default eventHandler(() => {
36+
throw createError({
37+
statusCode: 404,
38+
statusMessage: 'Page Not Found'
39+
})
40+
}
41+
```
42+
43+
::ReadMore{link="/guide/features/error-handling"}
44+
::
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# `showError`
2+
3+
Nuxt provides a quick and simple way to show a full screen error page if needed.
4+
5+
Within your pages, components and plugins you can use `showError` to show an error error.
6+
7+
**Parameters:**
8+
9+
- `error`: `string | Error | Partial<{ cause, data, message, name, stack, statusCode, statusMessage }>`
10+
11+
```js
12+
showError("😱 Oh no, an error has been thrown.")
13+
showError({ statusCode: 404, statusMessage: "Page Not Found" })
14+
```
15+
16+
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.
17+
18+
`showError` calls the `app:error` hook.
19+
20+
::ReadMore{link="/guide/features/error-handling"}
21+
::

docs/content/3.api/3.utils/throw-error.md

Lines changed: 0 additions & 20 deletions
This file was deleted.

examples/app/error-handling/app.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script setup lang="ts">
2-
import { throwError } from '#app'
2+
import { showError } from '#app'
33
const route = useRoute()
44
if ('setup' in route.query) {
55
throw new Error('error in setup')
@@ -30,7 +30,7 @@ function triggerError () {
3030
<NuxtLink to="/?middleware" class="n-link-base">
3131
Middleware
3232
</NuxtLink>
33-
<button class="n-link-base" @click="throwError">
33+
<button class="n-link-base" @click="showError">
3434
Trigger fatal error
3535
</button>
3636
<button class="n-link-base" @click="triggerError">
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export default defineNuxtRouteMiddleware((to) => {
22
if ('middleware' in to.query) {
3-
return throwError('error in middleware')
3+
return showError('error in middleware')
44
}
55
})

packages/nuxt/src/app/components/nuxt-root.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
<script setup>
99
import { defineAsyncComponent, onErrorCaptured } from 'vue'
10-
import { callWithNuxt, throwError, useError, useNuxtApp } from '#app'
10+
import { callWithNuxt, isNuxtError, showError, useError, useNuxtApp } from '#app'
1111
1212
const ErrorComponent = defineAsyncComponent(() => import('#build/error-component.mjs'))
1313
@@ -24,8 +24,8 @@ if (process.dev && results && results.some(i => i && 'then' in i)) {
2424
const error = useError()
2525
onErrorCaptured((err, target, info) => {
2626
nuxtApp.hooks.callHook('vue:error', err, target, info).catch(hookError => console.error('[nuxt] Error in `vue:error` hook', hookError))
27-
if (process.server) {
28-
callWithNuxt(nuxtApp, throwError, [err])
27+
if (process.server || (isNuxtError(err) && (err.fatal || err.unhandled))) {
28+
callWithNuxt(nuxtApp, showError, [err])
2929
}
3030
})
3131
</script>
Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,36 @@
1+
import { createError as _createError, H3Error } from 'h3'
12
import { useNuxtApp, useState } from '#app'
23

34
export const useError = () => {
45
const nuxtApp = useNuxtApp()
56
return useState('error', () => process.server ? nuxtApp.ssrContext.error : nuxtApp.payload.error)
67
}
78

8-
export const throwError = (_err: string | Error) => {
9-
const nuxtApp = useNuxtApp()
10-
const error = useError()
11-
const err = typeof _err === 'string' ? new Error(_err) : _err
12-
nuxtApp.callHook('app:error', err)
13-
if (process.server) {
14-
nuxtApp.ssrContext.error = nuxtApp.ssrContext.error || err
15-
} else {
16-
error.value = error.value || err
9+
export interface NuxtError extends H3Error {}
10+
11+
export const showError = (_err: string | Error | Partial<NuxtError>) => {
12+
const err = createError(_err)
13+
err.fatal = true
14+
15+
try {
16+
const nuxtApp = useNuxtApp()
17+
nuxtApp.callHook('app:error', err)
18+
if (process.server) {
19+
nuxtApp.ssrContext.error = nuxtApp.ssrContext.error || err
20+
} else {
21+
const error = useError()
22+
error.value = error.value || err
23+
}
24+
} catch {
25+
throw err
1726
}
1827

1928
return err
2029
}
2130

31+
/** @deprecated Use `throw createError()` or `showError` */
32+
export const throwError = showError
33+
2234
export const clearError = async (options: { redirect?: string } = {}) => {
2335
const nuxtApp = useNuxtApp()
2436
const error = useError()
@@ -28,3 +40,11 @@ export const clearError = async (options: { redirect?: string } = {}) => {
2840
}
2941
error.value = null
3042
}
43+
44+
export const isNuxtError = (err?: string | object): err is NuxtError => err && typeof err === 'object' && ('__nuxt_error' in err)
45+
46+
export const createError = (err: string | Partial<NuxtError>): NuxtError => {
47+
const _err: NuxtError = _createError(err)
48+
;(_err as any).__nuxt_error = true
49+
return _err
50+
}

packages/nuxt/src/app/composables/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ export { useAsyncData, useLazyAsyncData, refreshNuxtData } from './asyncData'
33
export type { AsyncDataOptions, AsyncData } from './asyncData'
44
export { useHydration } from './hydrate'
55
export { useState } from './state'
6-
export { clearError, throwError, useError } from './error'
6+
export { clearError, createError, isNuxtError, throwError, showError, useError } from './error'
7+
export type { NuxtError } from './error'
78
export { useFetch, useLazyFetch } from './fetch'
89
export type { FetchResult, UseFetchOptions } from './fetch'
910
export { useCookie } from './cookie'

packages/nuxt/src/app/plugins/router.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { parseURL, parseQuery, withoutBase, isEqual, joinURL } from 'ufo'
33
import { createError } from 'h3'
44
import { defineNuxtPlugin } from '..'
55
import { callWithNuxt } from '../nuxt'
6-
import { clearError, navigateTo, throwError, useRuntimeConfig } from '#app'
6+
import { clearError, navigateTo, showError, useRuntimeConfig } from '#app'
77
// @ts-ignore
88
import { globalMiddleware } from '#build/middleware'
99

@@ -228,7 +228,7 @@ export default defineNuxtPlugin<{ route: Route, router: Router }>((nuxtApp) => {
228228
const error = result || createError({
229229
statusMessage: `Route navigation aborted: ${initialURL}`
230230
})
231-
return callWithNuxt(nuxtApp, throwError, [error])
231+
return callWithNuxt(nuxtApp, showError, [error])
232232
}
233233
}
234234
if (result || result === false) { return result }

0 commit comments

Comments
 (0)