Struggling a bit to set up error handling with vuex. There seems to be quite a few ways to do so and little documentation on proper error handling. I've been experimenting with four alternatives, though I haven't found a satisfying solution yet.
Alternative 1 - Catching and processing errors on component
in pages/login.vue:
export default {
methods: {
onLogin() {
this.$store.dispatch('auth/login', {
email: this.email,
password: this.password,
}).then(() => {
this.$router.push('/home');
}).catch((error) {
// handle error in component
});
},
},
}
in store/auth.js:
export const actions = {
login({ commit }, { email, password }) {
return this.$axios.post('/api/login', {
email,
password,
}).then((res) => {
doSomething(res);
});
},
}
PROS
Hmm.
CONS
Errors not handled and stored in vuex.
Introduces lots of boilerplate code in component methods.
Alternative 2 - Catching and processing errors in vuex
in pages/login.vue:
export default {
methods: {
onLogin() {
this.$store.dispatch('auth/login', {
email: this.email,
password: this.password,
}).then(() => {
this.$router.push('/home');
});
},
},
}
in store/auth.js:
export const actions = {
login({ commit }, { email, password }) {
return this.$axios.post('/api/login', {
email,
password,
}).then((res) => {
doSomething(res);
}).catch((error) => {
// store error in application state
commit('SET_ERROR', error);
});
},
}
PROS
Error object is accessible with vuex from any component
Could use a reactive error component in layout, which is revealed when the error state changes.
CONS
I'm not sure if there is a way to track the source of the error, from which component it was thrown.
Alternative 3 - Catching errors using axios interceptors
in plugins/axios.js:
export default function({ $axios, store }) {
$axios.onError(error => {
store.dispatch('setError', error);
});
}
in pages/login.vue:
export default {
methods: {
onLogin() {
this.$store.dispatch('auth/login', {
email: this.email,
password: this.password,
}).then(() => {
this.$router.push('/home');
});
},
},
}
in store/auth.js:
export const actions = {
login({ commit }, { email, password }) {
return this.$axios.post('/api/login', {
email,
password,
}).then((res) => {
doSomething(res);
});
},
}
PROS
Global error handling
No need to catch errors in either vuex or component
No boiler-plate code
CONS
All exceptions are unhandled, meaning non-axios errors are uncaught.
Alternative 4 - Custom error plugin
I've been experimenting in implementing a custom plugin that catches all exceptions, but I'm not succeeding in making it work.
in plugins/catch.js:
export default (ctx, inject) => {
const catchPlugin = function (func) {
return async function (args) {
try {
await func(args)
} catch (e) {
return console.error(e)
}
}
};
ctx.$catch = catchPlugin;
inject('catch', catchPlugin);
}
in pages/login.vue:
export default {
methods: {
onLogin: this.$catch(async function () {
await this.$store.dispatch('auth/login', { email: this.email, password: this.password });
this.$router.push('/home');
}),
},
}
PROS
No boilerplate.
All errors caught in plugin.
CONS
I cannot make it work. :(
My impression is that there is a lack of documentation on error handling in vue/nuxt. Could anyone get the fourth alternative to work? Would this be ideal? Any other alternatives? What is conventional?
Thank you for your time!
The reason why option #4 is not working is because you're returning a function that never gets executed:
function catchPlugin(outerFunction) {
return function async innerFunction(args) {
try {
const data = await outerFunction(args);
return { data }
} catch (error) {
return { error }
}
}
}
Usage:
const execute = catchPlugin((args) => {
// do something
})
execute('myArgument');
As you can see you need to execute the inner function as well, to make your example work:
onLogin: this.$catch(async function () {
await this.$store.dispatch('auth/login', { email: this.email, password: this.password });
this.$router.push('/home');
})(), // mind the () :-)
But... I believe handling errors in components is not a bad thing, since this is tightly coupled to your view component. For instance, think about a login component, what we see these days is a global error handler (toastr) which will display a toast message if the username/password is incorrect. From my experience this is not the best behavior, it's a good starting point but better would be to add error messages close to the component displaying what exactly went wrong. Meaning you will always have to add error handling (UI related) in the component itself.
We're also struggling with this in our company with colleagues working on the same product. One is adding error handling, the other one is not.. The only solution, in my opinion, is to educate developers to always add proper error handling. The syntax with async/await is not that bad:
methods: {
async login (email, password) {
try {
await this.$store.dispatch('auth/login', { email, password })
// do something after login
} catch (error) {
// handle error
}
}
}
One last thing about your con: Errors not handled and stored in vuex.. Why is this a con? Do you need to have the error globally available? What I see a lot is people putting so much useless state in vuex that's only used in the component itself. It's not bad to have local component state. Since it's about login, this error should only be known in the login component.
Use Promise in action
Example in vuex:
NEW_AUTH({ commit }) {
return new Promise((resolve, reject) => {
this.$axios.$get('/token').then((res) => {
...
resolve();
}).catch((error) => {
reject(error);
})
})
}
In page:
this.$store.dispatch('NEW_AUTH')
.then(() => ... )
.catch((error) => ... )
To address the Con from Alternative 2 you can either
(a) pass in the name of the component or even a reference to the component or
(b) you can persist the error in the state for the component that made the call. Then in your component you could check if there is an error and display it. For that you could use a mixin to forgo the need for boiler plate.,
in store/auth.js:
export const actions = {
login({ commit }, { email, password }) {
return this.$axios.post('/api/login', {
email,
password,
}).then((res) => {
doSomething(res);
commit('save_to_state', { response: res });
}).catch((error) => {
commit('save_to_state', { error });
});
},
}
Create an error key in the state of each Vuex module. Then dispatch the error for a given component to its relative Vuex module. Then create a global handler to watch for errors in the separate Vuex modules and, if one is triggered, display the error.
// store/auth.js
export const state = () => {
return {
success: null,
error: null
}
}
export const actions = {
async login({ commit }, { email, password }) {
try {
const response = await axios.post('/api/login', {
email,
password
})
commit('SET_SUCCESS', response)
} catch(err) {
commit('SET_ERROR', error)
}
}
}
export const mutations = {
SET_SUCCESS(state, payload) {
state.success = payload
},
SET_ERROR(state, payload) {
state.error = payload
}
}
// auth.vue
export default {
methods: {
onLogin() {
try {
await this.$store.dispatch('auth/login', {
email: this.email,
password: this.password
})
if (this.$store.state.auth.success) this.$router.push('/home')
} catch (err) {
console.log(err)
}
}
}
}
// app.vue
export default {
created() {
this.$store.subscribe((mutation, state) => {
if (mutation.type.includes('ERROR')) {
// display error in global error output
console.log(mutation.payload)
}
})
}
}
Related
In my angular application I am sending a request to my backend to check credentials, after success the backend sends an token which I read. So far this works, but I had to use an pipe to make it map to a method and then make it work. But my problem now it even though I am getting 200 from the server my page will not navigate to the protected page automatically. If I enter the url manually it works this is what I tried:
authenticateUser(login: LoginModel){
this.http = new HttpClient(this.handler)
return this.http.post<JwtToken>(environment.rootUrl + 'api/authenticate', {
username: login.username,
password: login.password,
}).pipe(map(response => this.authenticateSuccess(response)))
.subscribe({
next: () => {
this.isAuthenticated = true;
this.router.navigate(['/dashboard'])
}, error: (error) => {
this.isAuthenticated = false;
console.log(error)
}
})
}
It does not enter the subscribe part after the pipe. Is there any way to make this work? I still want to have an error handling like if no error then navigate to url if error do not navigate.
EDIT:
AuthenticateSuccess method:
isUserLoggedIn(){
return !! localStorage.getItem('authenticationToken')
}
private authenticateSuccess(response: JwtToken): void {
const jwt = response.id_token;
localStorage.setItem('authenticationToken' , jwt)
this.localStorageService.store('authenticationToken', jwt);
console.log(this.localStorageService.retrieve('authenticationToken'))
this.sessionStorageService.clear('authenticationToken');
}
Authguard:
#Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate {
constructor(
private auth: AuthenticationService,
private router: Router
) {
}
canActivate(): Promise<boolean> {
return new Promise(resolve => {
if (this.auth.isUserLoggedIn()) {
resolve(true)
} else {
this.router.navigate(['authenticate'])
resolve(false)
}
})
}
}
SOLUTION:
authenticateUser(login: LoginModel) {
this.http = new HttpClient(this.handler)
return this.http.post<JwtToken>(environment.rootUrl + 'api/authenticate', {
username: login.username,
password: login.password,
}).subscribe({
next: response => {
this.isAuthenticated = true;
this.authenticateSuccess(response)
this.router.navigate(['/dashboard'])
}, error: (err) => {
console.log(err)
}, complete: () => {
console.log("finished without worry")
}
})
}
RxJs map operator is supposed to modify the content of an observable. The map operator however needs to return the same observable or another observable, for the next subscribe operation to be able to function.
In your case your map operator does not return any observable at all and therefore the subscribe method has no reason to be triggered.
You could simple return the response again in your method here
private authenticateSuccess(response: JwtToken): any {
const jwt = response.id_token;
localStorage.setItem('authenticationToken' , jwt)
this.localStorageService.store('authenticationToken', jwt);
console.log(this.localStorageService.retrieve('authenticationToken'))
this.sessionStorageService.clear('authenticationToken');
return response;
}
but I think all the code of the map method matches better directly inside the subscribe method.
I have created a redux that is going to request an API and if the result is 200, I want to redirect the user to another page using history.
The problem is: I don't know how to trigger this change if the action is a success.
I could redirect the user in my useCase function but I can't use history.push pathName/state argument because it only works in a React component.
So this is what I have done in my React component:
const acceptProposalHandler = () => {
store.dispatch(acceptProposal(id)).then(() => {
setTimeout(() => {
if (isAccepted) { //isAccepted is false by default but is changed to true if the
//request is 200
history.push({
pathname: urls.proposal,
state: {
starterTab: formatMessage({id: 'proposalList.tabs.negotiation'}),
},
});
}
}, 3000);
});
};
Sometimes it works but other times it wont. For some reason, .then is called even if the request fails.
I'm using setTimeOut because if I don't, it will just skip the if statement because the redux hasn't updated the state with isAccepted yet.
This is my useCase function from redux:
export const acceptProposal = (id: string) => async (
dispatch: Dispatch<any>,
getState: () => RootState,
) => {
const {auth} = getState();
const data = {
proposalId: id,
};
dispatch(actions.acceptProposal());
try {
await API.put(`/propostas/change-proposal-status/`, data, {
headers: {
version: 'v1',
'Content-Type': 'application/json',
},
});
dispatch(actions.acceptProposalSuccess());
} catch (error) {
dispatch(actions.acceptProposalFailed(error));
}
};
What I'm doing wrong? I'm using Redux with thunk but I'm not familiar with it.
".then is called even if the request fails." <- this is because acceptProposal is catching the API error and not re-throwing it. If an async function does not throw an error, it will resolve (i.e. call the .then). It can re-throw the error so callers will see an error:
export const acceptProposal = (id: string) => async (
// ... other code hidden
} catch (error) {
dispatch(actions.acceptProposalFailed(error));
// ADD: re-throw the error so the caller can use `.catch` or `try/catch`
throw error;
}
};
I am trying to implement the following logic: call login then if response is ok, call method for retrieving user data.
Login action
loginUser({commit,dispatch}, credentials) {
const form = new URLSearchParams();
form.append("login", credentials.login);
form.append("password", credentials.password);
const formConfig = {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
};
return Axios.post(loginUrl, form, formConfig).then(
(response) => {
commit('setErrorMessage', '', {root: true});
commit('setAuthenticated', response.headers[authorization]);
dispatch('getUserByLoginAuth',credentials.login);
},
(error) => {
if (error.response.status===500){
commit('setErrorMessage', error.response.data.message, {root: true});
} else {
commit('setErrorMessage', error.response.data, {root: true});
}
});
},
The second action dispatched from the one above:
getUserByLoginAuth({commit, getters}, login) {
return getters.authenticatedAxios.get(userUrl + '/find', {
params: {
login: login
}
}).then(
(response) => {
commit('setErrorMessage', '', {root: true});
commit('setUser', response.data);
},
(error) => {
commit('setErrorMessage', error.response.data, {root: true});
});
},
This action is called from the second time only(as I understand it is related to promise).
Here is a code from component which dispatches login action
this.$store.dispatch('loginUser', this.credentials).then(() => {
this.errorMessage = this.getError;
if (this.errorMessage.length) {
this.errorOccurred = true;
}
this.$router.push({path: '/user/' + this.getId});
});
this.errorOccurred = false;
},
Here also I am not sure if I am doing routing in correct place. As I understand then will work with promise from getUser so errorMessage from login might be lost. I would like to prevent it and make dispatch of getUser correctly from the first time
I don't entirely follow what you're asking but this seems likely to be a problem:
dispatch('getUserByLoginAuth',credentials.login);
The problem isn't the call itself. The problem is that it's kicking off a new asynchronous action without chaining it onto the existing promises. From the perspective of loginUser everything is done, it won't wait for getUserByLoginAuth.
The result will be that the then in your component will be called before getUserByLoginAuth is done. I would imagine this is why it seems to work the second time, because it's picking up the relevant data from the previous call.
The solution would be simply to change it to:
return dispatch('getUserByLoginAuth',credentials.login);
By putting in a return it adds it to the promise chain, so loginUser won't be treated as complete until getUserByLoginAuth is done.
I am fairly new to React and the use of Firebase. Right now I have a Firebase.js file where I have stored and exported all of my functions and consts. This is what it looks like:
export const auth = firebase.auth();
export function performLogin(email, password) {
auth.signInWithEmailAndPassword(email, password).then(function(data) {
//Works
}).catch(function(error) {
//Does not work
});
}
So, I have this login.js that I am calling performLogin from, and I was wondering what would be the best way of doing this? How can I create a callback, or at least read any kind of return message? This is how I call performLogin:
clickLogin(e){
e.preventDefault();
performLogin(this.state.email, this.state.password);
}
And this works, as the console output tells me if the login was successful or not. However, I want to be able to use this function in order to retrieve the login status, and then determine wether or not I should prompt an error message or push the user to the admin dashboard. How would I do this?
Is it possible to call performLogin like this?
performLogin(this.state.email,this.state.password,(callback)){
if (callback == true) { //Success } else { //Error }
}
Your performLogin function could take a third parameter, a callback function that lives where your clickLogin() method lives:
export function performLogin(email, password, callback) {
auth.signInWithEmailAndPassword(email, password).then(function(data) {
//Works
callback(data);
}).catch(function(error) {
//Does not work
callback({ error });
});
}
loginResult(result) {
if(result.error) {
//failed
} else {
//logged in
}
}
clickLogin(e){
e.preventDefault();
performLogin(this.state.email, this.state.password, this.loginResult);
}
Or you could return the promise that signInWithEmailAndPassword() returns and handle it in the component.
export function performLogin(email, password) {
return auth.signInWithEmailAndPassword(email, password);
}
clickLogin(e){
e.preventDefault();
performLogin(this.state.email, this.state.password)
.then(result => //logged in)
.catch(error => //failed)
};
Take a look at the function onAuthStateChanged from "firebase/auth" package.
Here is a link to the docs:
https://firebase.google.com/docs/auth/web/manage-users
I am trying to make my Vue app have server-side rendering. I am using vue-server-renderer (https://www.npmjs.com/package/vue-server-renderer). Client-side rendering is working fine.
My app use vue-router and axios
Here is my server.js:
server.get('*', (request, response) => {
bundleRenderer.renderToString({ url: request.url }, (error, htmlPromise) => {
if (error) {
// Log the error in the console
console.error(error)
// Tell the client something went wrong
return response
.status(500)
.send(error)
}
response.send(layout.replace('<div id=app></div>', htmlPromise))
})
})
getInfo() is the method to fetch server data.
Here is getInfo():
export default {
methods: {
getInfo(api) {
return axios
.get(api || this.$route.params.path)
.then((data) => {
this.data = data
this.$set(this, 'isLoading', false)
})
},
},
}
My server entry is:
import { app, router, store } from './index'
export default context => {
let componentPromises = router.getMatchedComponents().filter((component) => {
return component.methods && component.methods.getInfo
}).map((component) => {
return component.methods.getInfo()
})
return Promise.all(componentPromises).then(() => {
return app
})
}
However, I soon realize that all the components from router.getMatchedComponents() does not have $route or $set. Therefore, the method getInfo() stops working.
The document from https://router.vuejs.org/en/api/router-instance.html is very short and does not provide much information:
router.getMatchedComponents()
Returns an Array of the components (definition/constructor, not
instances) matched by the current route. This is mostly used during
server-side rendering to perform data prefetching.
How can I fix the problem?
I have previously incurred into a similar problem and managed to successfully prefetch data by doing the following:
app.$router.onReady(() => {
const matchedComponents = app.$router.getMatchedComponents()
if (!matchedComponents.length) { /* ... */}
Promise.all(matchedComponents.map((Component: any) => {
if (Component.options.methods.asyncData) {
return Component.options.methods.asyncData({
store: app.$store,
route: app.$router.currentRoute
});
}
})).then(() => { /* your callback here ... */ });
}
According to vue ssr documentation (https://ssr.vuejs.org/en/data.html) the suggested way is to use a custom asyncData method in your component to perform data fetching rather than calling component methods directly:
export default {
asyncData ({ store, route }) {
// return the Promise from the action
return store.dispatch('fetchItem', route.params.id)
}
},