Published: June 08 2022

Vue 3 + VeeValidate - Display Custom Error for Failed HTTP API Request

Tutorial built with Vue 3.2.33 and VeeValidate 4.5.11

This is a quick example of how to display a custom error message in Vue 3 with VeeValidate after a failed HTTP (AJAX) request to an API.

The below component is part of a Vue 3 JWT authentication tutorial I posted recently that includes a live demo, so to see the code running check out Vue 3 + Pinia - JWT Authentication Tutorial & Example.

When invalid credentials are submitted in the form, the error message 'Username or password is incorrect' is returned by the API and displayed below the login button.


Vue 3 LoginView Component

Path: /src/views/LoginView.vue

The login view contains a form built with the VeeValidate library that contains username and password fields for logging into the Vue 3 app.

Form validation rules are defined with the Yup schema validation library which VeeValidate supports out of the box, for more info on Yup see

Set custom error in form onSubmit() method

The onSubmit() method posts the user credentials to the API by calling authStore.login(). If the login request fails a custom error apiError is set on the form using the VeeValidate setErrors() function, and the returned API error is rendered in the Vue template via the VeeValidate errors object - {{errors.apiError}}.

On login success the returned user data is stored in Pinia global state by the login() action method in the auth store, the method then redirects the user to the home page.

The Vue component template contains the form with input fields and validation messages. The form and fields are built with the VeeValidate <Form /> and <Field /> components which automatically hook into the validation rules (schema) based on the name of the field.

The form calls the onSubmit() method when the form is submitted and valid. Validation rules are bound to the form with the validation-schema prop, and validation errors are provided to the form template via the scoped slot v-slot="{ errors }". For more info on form validation with Vue 3 and VeeValidate see Vue 3 + VeeValidate - Form Validation Example (Composition API).

<script setup>
import { Form, Field } from 'vee-validate';
import * as Yup from 'yup';

import { useAuthStore } from '@/stores';

const schema = Yup.object().shape({
    username: Yup.string().required('Username is required'),
    password: Yup.string().required('Password is required')

function onSubmit(values, { setErrors }) {
    const authStore = useAuthStore();
    const { username, password } = values;

    return authStore.login(username, password)
        .catch(error => setErrors({ apiError: error }));

        <div class="alert alert-info">
            Username: test<br />
            Password: test
        <Form @submit="onSubmit" :validation-schema="schema" v-slot="{ errors, isSubmitting }">
            <div class="form-group">
                <Field name="username" type="text" class="form-control" :class="{ 'is-invalid': errors.username }" />
                <div class="invalid-feedback">{{errors.username}}</div>
            <div class="form-group">
                <Field name="password" type="password" class="form-control" :class="{ 'is-invalid': errors.password }" />
                <div class="invalid-feedback">{{errors.password}}</div>
            <div class="form-group">
                <button class="btn btn-primary" :disabled="isSubmitting">
                    <span v-show="isSubmitting" class="spinner-border spinner-border-sm mr-1"></span>
            <div v-if="errors.apiError" class="alert alert-danger mt-3 mb-0">{{errors.apiError}}</div>

Pinia Auth Store

Path: /src/stores/

The auth store contains Pinia state and actions for authentication. The user state property holds the current logged in user, it is initialized with the 'user' object from local storage to support staying logged in between page refreshes and browser sessions, or null if localStorage is empty.

The Pinia login() action method posts credentials to the API, on success the returned user object is stored in Pinia state and localStorage, and the router redirects to the return url or home page. On fail the async method throws an error which is caught and displayed inside the LoginView component.

The logout() action method sets the user to null in Pinia state, removes it from localStorage and redirects to the login page.

import { defineStore } from 'pinia';

import { fetchWrapper, router } from '@/helpers';

const baseUrl = `${import.meta.env.VITE_API_URL}/users`;

export const useAuthStore = defineStore({
    id: 'auth',
    state: () => ({
        // initialize state from local storage to enable user to stay logged in
        user: JSON.parse(localStorage.getItem('user')),
        returnUrl: null
    actions: {
        async login(username, password) {
            const user = await`${baseUrl}/authenticate`, { username, password });

            // update pinia state
            this.user = user;

            // store user details and jwt in local storage to keep user logged in between page refreshes
            localStorage.setItem('user', JSON.stringify(user));

            // redirect to previous url or default to home page
            router.push(this.returnUrl || '/');
        logout() {
            this.user = null;

Need Some Vue 3 Help?

Search fiverr for freelance Vue 3 developers.

Follow me for updates

On Twitter or RSS.

When I'm not coding...

Me and Tina are on a motorcycle adventure around Australia.
Come along for the ride!


Supported by