The Nuxt.js error page, as described here, is passed an error property which can be used to figure out what the error was and display something appropriate.
However, the error object passed looks something like:
{
message:"Cannot read property 'nonexistentThing' of undefined",
statusCode:"TypeError"
}
I would like to take action on the error page (hitting an error-reporting API) which ideally would have access to the underlying error object including the backtrace etc.
Is there a way to either
access this from the error page; or
make a plugin which intercepts the error while Nuxt’s own error-handling is in flight?
Although I didn't found a way to access the underlying error object I've found this error method from nuxt where you can throw your exception which finally gets displayed in Nuxt's error page. You can access this error method in the context object (https://nuxtjs.org/api/context/)
An example error including the exception could be this:
export default {
asyncData ({ params, error }) {
return axios.get(`https://my-api/posts/${params.id}`)
.then((res) => {
return { title: res.data.title }
})
.catch((e) => {
error({ statusCode: 404, message: 'Post not found', myError: e })
})
}
}
So then you could process the exception in the error page like this
<template>
<div class="container">
<h1 v-if="error.statusCode === 404">Page not found</h1>
<h1 v-else>An error occurred</h1>
<nuxt-link to="/">Home page</nuxt-link>
</div>
</template>
<script>
export default {
props: ['error'],
mounted() {
const { statusCode, message, myError } = this.error;
// send myError to error-reporting API
}
}
</script>
Related
I came across this vue-concurrency which looks like a great library for handling asyc calls.
But I'm having trouble understanding how to implement it.
Here is a simple Vue component using vue-concurrency:
<template>
<div>
<div v-if="task.isRunning">Loading...</div>
<div v-else-if="task.isError">{{ task.last?.error.message }}</div>
<div v-else>
{{ task.last?.value }}
</div>
</div>
</template>
<script setup lang="ts">
import { timeout, useTask } from 'vue-concurrency';
const task = useTask(function* () {
yield timeout(1000);
if (Math.random() < 0.5) {
// lets say the API is flaky and errors out often:
throw new Error('Ruh oh. Something went wrong.');
} else {
return 'tada';
}
});
const instance = task.perform();
// Now I try to access some of the varibles
console.log(instance.isError); // runs immediatley, always returns "false"
console.log(instance.error); // runs immediatley, always returns "null"
console.log(instance.value); // runs immediatley, always returns "null"
console.log(task.isError); // runs immediatley, always returns "false"
console.log(task.last?.isError); // runs immediatley, always returns "false"
console.log(task.last?.error); // runs immediatley, always returns "null"
console.log(task.last?.value); // runs immediatley, always returns "null"
</script>
This component makes a fake API call. Pauses for one second. Then will either throw an Error or return the "tada" string.
The current code works in the template, it shows "Loading..." and then either "tada" or the error message.
But I want to access the task and instance variables inside the script tag, and at the moment they all run immediatley without waiting for the async call to execute.
I thought I could fix this by using await task.perform(). Doing so gives me the task.last?.value variable. But the template stops working (becomes blank) and the error variables don't print to the console.
So how do I correctly access the state values of a vue-concurrency Task/Instance inside the script tag?
I understand this better now.
A Task creates one or more Instances, and those Instances are asynchronous. Therefore we need to handle Instances like any other async code (await, try/catch, then/catch, etc). I have included a few options for doing that.
These code examples replace everything below the line // Now I try to access some of the varibles of the opening post. The catch errors have TypeScript types and avoid EsLint errors where possible:
Option 1 - then/catch promises:
instance
.then((response) => {
console.log(response); // tada
console.log(instance.value); // tada
})
.catch((err: Error) => {
console.log(err.message); // 'Ruh oh. Something went wrong.'
console.log(instance.isError); // true
console.log(instance.error); // the Error object
// eslint-disable-next-line #typescript-eslint/no-unsafe-member-access
console.log(instance.error.message); // 'Ruh oh. Something went wrong.'
});
Option 2 - try/catch using async wrapper:
const wrapper = async () => {
try {
await instance;
console.log(instance.value); // tada
} catch (err) {
if (err instanceof Error) {
console.log(err.message); // 'Ruh oh. Something went wrong.'
}
console.log(instance.isError); // true
console.log(instance.error); // the Error object
// eslint-disable-next-line #typescript-eslint/no-unsafe-member-access
console.log(instance.error.message); // 'Ruh oh. Something went wrong.'
}
};
void wrapper();
Option 3 - IIFE:
(async () => {
await instance;
console.log(instance.value); // tada
})().catch((err: Error) => {
console.log(err.message); // 'Ruh oh. Something went wrong.'
console.log(instance.isError); // true
console.log(instance.error); // the Error object
// eslint-disable-next-line #typescript-eslint/no-unsafe-member-access
console.log(instance.error.message); // 'Ruh oh. Something went wrong.'
});
And actually the original code can be modified because we are not calling the Instance multiple times.
A streamlined version could chain the useTask() with perform() which would then return a single Instance (as opposed to the original code which returned a Task and later assigned the Instance to a variable).
That would save some lines but also require us to update the template and remove references to the task:
<template>
<div>
<div v-if="instance.isRunning">Loading...</div>
<div v-else-if="instance.isError">{{ instance.error.message }}</div>
<div v-else>
{{ instance.value }}
</div>
</div>
</template>
<script setup lang="ts">
import { timeout, useTask } from 'vue-concurrency';
const instance = useTask(function* () {
yield timeout(1000);
if (Math.random() < 0.5) {
// lets say the API is flaky and errors out often:
throw new Error('Ruh oh. Something went wrong.');
} else {
return 'tada';
}
}).perform();
// Can include then/catch, try/catch etc here if you want to access the instance variables
</script>
I'm working on error page. In layout directory I have error.vue and error_layout.vue.
Say I'm on Login page and an error occurs (Network error for example, I'm handling server and validation errors in login page). It shows my error page, but URL is not changed, it is: mydomain.com/login
The problem here is that I want to have correct URL for example mydomain.com/error
I want this because I want to be able to return user on previous page (in my case - mydomain.com/login), but refresh shows error page with wrong URL. I can't go back.
$nuxt.$route gives me login route.
UPDATE:
Here is how I handle server errors, this is my interceptor plugin:
export default function ({ $axios, redirect, app, error: nuxtError }) {
$axios.onError((error) => {
if (error.response) {
if (error.response.status === 423) {
redirect({
name: 'confirm-password',
query: { redirect_to: app.router.currentRoute.name }
})
}
} else {
// Unknown error, for example Network Error
// HERE IS MY PROBLEM
nuxtError({
statusCode: 503,
message: error.message
})
return Promise.resolve(false)
}
})
If the error comes from the server, then you can catch the error, then redirect the client to error page. For example if you are using axios then something like this:
await this.$axios.post('/login-url', userCreds)
.catch(e => {
this.$router.push({
path: '/error'
})
})
We use Vue with apollo and I have difficulties to handle errors properly.
This is on of our components
<template>
<div v-if="product">
<router-view :key="product.id" :product="product" />
</div>
<div v-else-if="!this.$apollo.loading">
<p>Product is not available anymore</p>
</div>
</template>
<script>
import productQuery from "#/graphql/product.graphql";
export default {
name: "ProductWrapper",
props: ["productId"],
apollo: {
product: {
query: productQuery,
variables() {
return {
productId: this.productId,
};
},
},
},
};
</script>
If the product is not available anymore, we have three options in the backend:
a) the backend just can send null without errors
b) send an error object as part of the data with unions
c) send some error extensions with apollo for easy error handling in the client
Option a) seems to be a strange option
Option b) is too complicated for our use case.
So I decided for option c):
In our backend we use apollo error extensions to send some proper errorCode for the client to handle.
{
data: { }
errors: [
{
"message": "NOT_FOUND",
"locations": ...,
"path": ...
"extensions": {
"details": [],
"errorCode": "NOT_FOUND",
"message": "NOT_FOUND",
"classification": "DataFetchingException"
}
]
}
Everything works fine as product results in null anyway as no data is sent, just an error.
But vue-apollo is logging this to console.error. I don't want any logging to console.error as the user sees an red mark in his browser. But I want it to pop up in the console.error if nobody else has handled this error.
I can add an error handling in three places:
error() inside query definition
$error() default handler for all queries
ErrorLink
ErrorLink seems to be the wrong place as only the query in the component knows that NOT_FOUND is not fatal but can happen sometimes. Same is true for $error
So how do I say: this error might happen, I am well prepared for this. All other errors should be handled by the ErrorLink. How can I consume an error in my component?
My overview over vue-apollo error handling.
SmartQuery error handler
Documentation: https://apollo.vuejs.org/api/smart-query.html
error(error, vm, key, type, options) is a hook called when there are
errors. error is an Apollo error object with either a graphQLErrors
property or a networkError property. vm is the related component
instance. key is the smart query key. type is either 'query' or
'subscription'. options is the final watchQuery options object.
Not documented: If you return false the error processing is stopped and the default error handler (Apollo Provider) is not called. If you return true or do not return anything (aka undefined) the default error handler is called.
Code Example
export default {
name: "ProductWrapper",
props: ['productId'],
apollo: {
product: {
query: productQuery,
variables() {
return {
productId: this.productId
}
},
error(errors) {
console.log("Smart Query Error Handler")
console.log(errors.graphQLErrors)
return false;
}
}
}
}
VueApolloProvider errorHandler
Documentation: https://apollo.vuejs.org/api/apollo-provider.html
Global error handler for all smart queries and subscriptions
Important: This is NOT for mutations.
Not documented: This is not called when an error handler of a smart query returned false
Code Example
const apolloProvider = new VueApollo({
defaultClient: apolloClient,
errorHandler: (errors) => {
console.log("Provider errorHandler")
console.log(errors)
}
});
VueApolloProvider $error special option (useless)
Documentation
https://apollo.vuejs.org/guide/apollo/special-options.html
$error to catch errors in a default handler (see error advanced options > for smart queries)
Code Example (wrong)
const apolloProvider = new VueApollo({
defaultClient: apolloClient,
defaultOptions: {
error(errors) {
console.log("Provider $error")
console.log(errors)
}
}
});
This was my first try, but it results in a call when the component is mounted and gives us the complete component. See https://github.com/vuejs/vue-apollo/issues/126
Code Example (kind of ok?)
const apolloProvider = new VueApollo({
defaultClient: apolloClient,
defaultOptions: {
$error() {
return (errors) => {
console.log("Provider $error handler")
console.log(errors.graphQLErrors)
}
},
},
});
This way the $error function is called.
But it is called just like the default errorHandler (see above). the documentation seems to be wrong. So this method looks rather useless to me. As you can see in this debugging screenshot, it is used just like the other error handlers:
ErrorLink
Documentation: https://www.apollographql.com/docs/react/data/error-handling/#advanced-error-handling-with-apollo-link
Code example
import {onError} from "apollo-link-error";
const errorHandler = onError(({ networkError, graphQLErrors }) => {
console.log("Link onError")
console.log({ graphQLErrors, networkError})
})
const link = split(
// split based on operation type
({query}) => {
const definition = getMainDefinition(query);
return definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
},
wsErrorHandler.concat(wsLink),
errorHandler.concat(httpLink)
);
Important: this is called before any other ErrorHandler and it works for queries, subscriptions and mutations
Mutation catch
Documentation https://apollo.vuejs.org/guide/apollo/mutations.html
Code example 1
async submit() {
await this.$apollo.mutate({
mutation: productDeleteMutation,
variables: {
productId: this.product.id
},
}).catch((errors) => {
console.log("mutation .catch error")
console.log(errors)
})
}
Code example 2
You can use try/catch too:
async submit() {
try {
await this.$apollo.mutate({
mutation: productDeleteMutation,
variables: {
productId: this.product.id
},
})
} catch (errors) {
console.log("mutation try/catch error")
console.log(errors)
}
}
the only other handler which can be called is the onError ErrorLink (see above), which would be called before the catch error handling.
Mutation handled by vue with errorCaptured
Add this lifecycle hook to your component to catch errors from mutations.
errorCaptured(err, vm, info) {
console.log("errorCaptured")
console.log(err.graphQLErrors)
return false
}
Documentation: https://v2.vuejs.org/v2/api/#errorCaptured
More links: https://medium.com/js-dojo/error-exception-handling-in-vue-js-application-6c26eeb6b3e4
It works for mutations only as errors from smart queries are handled by apollo itself.
Mutation errors with vue global errorHandler
You can have a vue default error handler to catch graphql errors from mutations
import Vue from 'vue';
Vue.config.errorHandler = (err, vm, info) => {
if (err.graphQLErrors) {
console.log("vue errorHandler")
console.log(err.graphQLErrors)
}
};
It works for mutations only as errors from smart queries are handled by apollo itself.
Documentation https://v2.vuejs.org/v2/api/#errorHandler
window.onerror
Errors outside vue can be handled with plain javascript
window.onerror = function(message, source, lineno, colno, error) {
if (error.graphQLErrors) {
console.log("window.onerror")
console.log(error.graphQLErrors)
}
};
This does not work for mutations, queries or subscriptions inside or outside vue components as these are handle by vue or apollo itself (and printed to console.err if not handled)
Documentation https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/onerror
Summary
The only way to catch errors for mutations AND queries at the same time is onError with an ApolloLink. Server, Network and Authentication errors should be catched there as these are not specific to any operation.
Additional notes
Approach how to handle errors in the backend
https://blog.logrocket.com/handling-graphql-errors-like-a-champ-with-unions-and-interfaces/
I have just introduced error handling to one of my Nuxt pages and apparently the action mapped and called inside fetch raises a not a function error. If the try/catch block isn't there it works as expected and there's no error at all.
Here is my component stripped to the essential parts:
export default {
name: 'ViewArticle',
async fetch ({ error }) {
try {
await this.fetchArticle({ articleSlug: this.articleSlug })
} catch (err) {
error({ statusCode: 404, message: 'May the force be with you' })
}
},
computed: {
...mapGetters({
article: 'article/single'
}),
articleSlug () {
return this.$route.params.articleSlug
}
},
methods: {
...mapActions({
fetchArticle: 'article/fetchOne'
})
}
}
I am assuming that somehow mapActions only gets executed later in the spiel, but can't figure out how to prevent the error. This way, basically every time I load the page it gets immediately redirected to the error page.
The error message I'm getting is the following. Obviously fetchArticle is a function, and unless it's inside the try/catch block, it works as expected.
this.fetchArticle is not a function 03:30:51
at Object.fetch (52.js:32:18)
at server.js:2881:39
at Array.map (<anonymous>)
at module.exports../.nuxt/server.js.__webpack_exports__.default (server.js:2864:51)
Fetch provides the context as argument.
fetch(context)
Inside the context we can find our store. Here you can take a look what context contains: https://nuxtjs.org/api/context
fetch(context) {
let store = context.store;
}
People like to destructure it
fetch({ store }) {}
Your code should look like this:
async fetch ({ error, store }) {
try {
await store.dispatch('article/fetchOne', { articleSlug: this.articleSlug })
} catch (err) {
error({ statusCode: 404, message: 'May the force be with you' })
}
},
Fetch gets executed on the server side, thats why you get is not an function error. Its undefined
... fetch is called on server-side...
Use async fetch({store})
async fetch ({ error, store }) {
try {
await store.dispatch( 'article/fetchOne' , { articleSlug: this.articleSlug })
} catch (err) {
error({ statusCode: 404, message: 'May the force be with you' })
}
I am using nested routes from nuxt.js:
https://nuxtjs.org/guide/routing#nested-routes
with a child component:
https://nuxtjs.org/api/components-nuxt-child/
To make it easer to understand:
I have an events page and a nested postView component.
Left you can see a calendar view which stays even if the route changes.
To the right I have the postdetails, which are replaced (nuxt child component):
With Nuxt this folder/file setup will setup my router correctly.
This works so far, but now I want to handle the case if somebody enters a non-existent postSlug in the url.
So far, it would just not be displayed, but I want to have a proper 404 code.
Do I have to set this manually? (I am currently making an axios call from the calendarview to see if the post.json exists. If so, I load it into the vuex store)
…
// if it is not in store get it via axios
else {
const ax = axios.create({
baseURL: `${window.location.origin}/events`
})
ax.get(`event.${this.$route.params.postSlug}.json`)
.then((response) => {
const newActivePost = response.data && response.data.items ? response.data.items.find(p => p.slug === this.$route.params.postSlug) : false
if (newActivePost) {
this.activePost = newActivePost
this.$store.dispatch('data/saveCompletePosts', this.activePost)
}
})
.catch((error) => {
console.log(error.response.data)
console.log(error.response.status)
console.log(error.response.headers)
})
}
But with this I would never get an error. Even if the json file does not exist. I always get a 200 status code.
I am not sure why this is the case, but maybe the response just contains the page without the post itself. Actually the same if I call the url with the missing post. If I have a look at the response string:
<body data-n-head="">
<div data-server-rendered="true" id="__nuxt">
<div class="nuxt-progress" style="width:0%;height:2px;background-color:#000000;opacity:0;"></div>
<div id="__layout">
<div class="Layout">
<!---->
<div class="SkipNavigationLink">Springe direkt zum Hauptinhalt</div>
<div class="no-ssr-placeholder"></div>
<main id="main" class="Layout__content">
<!---->
</main>
<div class="Footer">
<div class="Footer__logo-wrapper">
<div class="Logo"><img src="/_nuxt/img/logo-dark.0b2ed1f.svg" alt="Logo Image" class="Logo__image"></div>
</div>
You can see that there is the layout and the footer but no content.
My guess would be that if I could return a proper 404 status code in the nested route/child component I would also get a 404 in my axios response.
One – maybe hacky idea – would be to manually set the status code:
ax.get(`event.${this.$route.params.postSlug}.json`)
.then((response) => {
const newActivePost = response.data && response.data.items ? response.data.items.find(p => p.slug === this.$route.params.postSlug) : false
if (newActivePost) {
this.activePost = newActivePost
this.$store.dispatch('data/saveCompletePosts', this.activePost)
} else {
// SET STATUS CODE MANUALLY?
response.request.status = 404
}
})
And then handle the thing differently.
But I don't know if that is a good idea...
Some help about nuxt routing/404 handling is much appreciated.
cheers
-- - - - - - - Edit
I just found this
https://nuxtjs.org/api/pages-validate
I think this would actually call a 404 but I would not know how to validate, because there is no pattern in the postSlug.
I really would have to check with all the json files of the posts...
-- - - - - - Edit 2:
I can't even do that:
TypeError: Cannot assign to read only property 'status' of object '#<XMLHttpRequest>'
🤷♂️
I think you are looking for error function from context
async asynData({ params, store, error }) {
try {
await axioscall here
} catch (e) {
error({ statusCode: 404, message: 'Page not found' })
}
},
Not sure if it is related, I had redirect to error page working in top level pages directory (pages/_.vue), something like this:
async asyncData(context) {
...
try {
const response = await context.$axios(options);
return {
isPostLoaded: true,
loadedPost: response.data.data.postBy
}
} catch (error) {
context.error({ statusCode: 404, message: 'Page not found' });
}
}
but it didn't work in nested directory in pages (pages/posts/_slug/index.vue)
adding this seem to have helped, I checked if response data was available (might not be the best way but seem to work)
if (response.data.data.postBy === null) {
throw new Error();
}
...
async asyncData(context) {
...
try {
const response = await context.$axios(options);
if (response.data.data.postBy === null) {
throw new Error();
}
return {
isPostLoaded: true,
loadedPost: response.data.data.postBy
}
} catch (error) {
context.error({ statusCode: 404, message: 'Page not found' });
}
}