Errors, Security and Deployment

By Syncfusion®, Inc. · Last updated

onErrorCaptured and error boundaries, error propagation, app.config.errorHandler and warnHandler, friendly states for network, HTTP and validation errors, untrusted templates and v-html, validating external data, public env variables and secrets, folder structure and naming, .env files, modes and typed env, and building and deploying with a base path and SPA fallback, ESLint and Prettier, and a CI pipeline.

Shipping a Real App

  • A finished app must survive bad networks, bad data and bad luck.
  • It must not let strangers run code on your page or read your secrets.
  • And it must be organised so a team can work on it, and built so a host can serve it.
  • This lesson covers all three: errors, security and deployment.

Simple lesson: a production app handles errors kindly, stays safe, and is easy to build and host.

A Real-World Example

  • Meera's menu page has a "Today's offers" widget that sometimes crashes.
  • Without care, one broken widget can blank the whole page.
  • An error boundary catches the crash and shows a small message instead.
  • What happened: OffersWidget crashed, because offers was missing.
  • The boundary caught it with onErrorCaptured, showed a message, and the rest of the page stayed.

What You Will Learn

  1. How onErrorCaptured and error boundaries work.
  2. How errors travel up the component tree.
  3. A global handler with app.config.errorHandler.
  4. Friendly states for network, HTTP and validation errors.
  5. How to check data from outside the app.
  6. XSS, v-html and untrusted templates.
  7. Why VITE_ variables are public, and where secrets belong.
  8. Folder structure, linting, formatting, .env files and modes.
  9. CI, building, the base path, SPA fallback and hosting.

Catching Errors

  • onErrorCaptured((err, instance, info) => ...) runs when a child throws during render, a hook, a watcher or an event handler.
  • Return false to stop the error from going further up.
  • Otherwise it keeps travelling up to the next boundary, then to app.config.errorHandler.
  • info says where it happened, such as "render function" or "setup function".

Animation: an error thrown in OffersWidget travels up to ErrorBoundary, which returns false and stops it. Without that, it would continue to App and then to app.config.errorHandler

OffersWidget throws ErrorBoundary App app errorHandler

return false: stop here without return false, it keeps going up ↑

Errors travel up through boundaries until one stops them, then reach the global handler.

The Global Handler

  • app.config.errorHandler = (err, instance, info) => ... catches every error no boundary stopped.
  • Send errors to a reporting service from here, such as Sentry.
  • app.config.warnHandler catches Vue warnings in development.
  • Errors outside Vue, such as in a setTimeout or an un-awaited Promise, never reach it. Catch those with window.addEventListener("unhandledrejection", ...) and "error".

onErrorCaptured and errorHandler do not catch errors in your own setTimeout callbacks or in Promises you never await. Use try/catch in async code, and listen for unhandledrejection on window for the rest.

Simple lesson: boundaries catch errors near where they happen. errorHandler catches the rest.

Friendly Error States

  • Network error: no connection. Say "You seem to be offline" and offer to retry.
  • HTTP error: the server said 404 or 500. Explain in plain words, without codes for normal users.
  • Validation error: the user's input was wrong. Say which field and how to fix it, as in Events and Forms.
  • Never show a blank page or a raw stack trace.

Security

XSS and Untrusted Content

  • XSS = a stranger's text runs as code on your page. You met it in Templates and Directives.
  • {{ }} always escapes text. It is safe for any user input.
  • v-html is not. Use it only for HTML you trust, or after cleaning it with a sanitiser such as DOMPurify.
  • Never compile a template that a user wrote. Templates can run any JavaScript.
  • Never put user input into :href without checking it. javascript: links run code.
  • What happened: the review was shown as text. No code ran.
  • The javascript: link was replaced with #, because only http and https were allowed.

Validating Data from Outside

  • Data from a server, localStorage or the URL may not have the shape you expect.
  • TypeScript types are removed at run time. They cannot stop bad data.
  • Check the shape yourself before using it, with a type guard, or a library such as Zod.

Secrets and Env Variables

  • Variables in .env that start with VITE_ are built into the JavaScript the browser downloads.
  • Anyone can read them in DevTools. They are public.
  • Put only public settings there: the API address, a public map key with domain limits.
  • Real secrets (database passwords, payment keys) stay on the server. The browser asks the server, and the server uses the secret.

Never put a secret in a VITE_ variable, or anywhere in your front-end code. "Hidden" in a minified file still means public.

Content Security Policy

  • A Content Security Policy (CSP) = a rule list the server sends, telling the browser which scripts and styles may run.
  • It blocks most XSS even if a bug lets bad HTML in.
  • A Vite build works with a strict CSP, because templates are compiled ahead of time. The full build with compile would need unsafe-eval, another reason not to compile templates in the browser.
# An example response header from your host
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' https://api.meeraskitchen.dev

Simple lesson: {{ }} for user text, check outside data, and keep secrets on the server.

Project Structure

FolderHoldsExample
src/views/Page components used by the routerMenuPage.vue
src/components/Reusable partsDishCard.vue, BaseButton.vue
src/composables/use functionsuseFetch.ts
src/stores/Pinia storescart.ts
src/services/ or src/api/API callsdishes.ts
src/router/Routes and guardsindex.ts
src/utils/Plain helper functionsmoney.ts
src/assets/Images and CSS processed by Vitelogo.svg
  • Components: PascalCase, two or more words. Composables: useX. Stores: useXStore.
  • Keep one component per file, named like the file.

Linting and Formatting

  • ESLint finds mistakes: unused variables, missing :key, a prop changed by a child.
  • Prettier formats code the same way for everyone, so reviews talk about logic, not spaces.
  • create-vue sets both up when you tick them. The ESLint config looks like this:
// eslint.config.ts
import pluginVue from "eslint-plugin-vue";
import { defineConfigWithVueTs, vueTsConfigs } from "@vue/eslint-config-typescript";
import skipFormatting from "@vue/eslint-config-prettier/skip-formatting";

export default defineConfigWithVueTs(
  { files: ["**/*.{ts,mts,tsx,vue}"] },
  { ignores: ["**/dist/**", "**/coverage/**"] },
  pluginVue.configs["flat/recommended"],
  vueTsConfigs.recommended,
  skipFormatting, // Prettier formats; ESLint does not fight it
);
npm run lint      # eslint . --fix
npm run format    # prettier --write src/
  • flat/essential catches errors only. flat/recommended also checks style and naming rules.
  • In VS Code, install the ESLint, Prettier and Vue (Official) extensions and turn on "format on save".

Checking Every Change (CI)

  • CI (continuous integration) = a server runs your checks on every push, before anything is deployed.
  • A typical Vue pipeline: install → lint → type-check → test → build.
  • If any step fails, the change does not ship.
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npm run lint
      - run: npx vue-tsc --noEmit
      - run: npx vitest run
      - run: npm run build
  • Most hosts (Netlify, Vercel, Cloudflare Pages) also deploy a preview URL for each pull request.

Simple lesson: lint and format on save, and let CI run lint, types, tests and build before every deploy.

.env Files and Modes

  • .env = loaded in every mode. .env.development and .env.production add mode-specific values.
  • .env.local = your own machine only. Never commit it.
  • import.meta.env gives MODE, DEV, PROD, BASE_URL and every VITE_ variable.
  • npm run dev uses the development mode. npm run build uses production.
  • In TypeScript, describe your variables in env.d.ts, so typos are caught:
// env.d.ts
/// <reference types="vite/client" />

interface ImportMetaEnv {
  readonly VITE_API_URL: string;
  readonly VITE_SHOP_NAME: string;
}

interface ImportMeta {
  readonly env: ImportMetaEnv;
}

Building and Deploying

  • npm run build writes optimised files to dist/: index.html, JavaScript and CSS with hashed names.
  • Hashed names (index-3f9a1c.js) let hosts cache files forever. A new build gets new names.
  • npm run preview serves dist/ locally to check the build.
  • Any static host works: Netlify, Vercel, Cloudflare Pages, GitHub Pages, or a CDN bucket.

Base Path

  • If the app lives in a sub-folder, such as https://example.com/kitchen/, set base in vite.config.ts.
  • Vue Router uses it too: createWebHistory(import.meta.env.BASE_URL).
// vite.config.ts
export default defineConfig({
  plugins: [vue()],
  base: "/kitchen/",
});

SPA Fallback

  • With web history, a visitor may open /menu directly, or refresh on it.
  • The host has no menu file, so it would answer 404.
  • Tell the host to answer every unknown path with index.html. Vue Router then shows the right page.
# Netlify: public/_redirects
/*    /index.html   200

# nginx
location / {
  try_files $uri $uri/ /index.html;
}

Before going live: run the tests, build, preview, check Lighthouse, open a deep link like /menu/3 directly, refresh it, and test on a phone.

Simple lesson: build to dist/, set base for sub-folders, and send every path to index.html.

Installable Apps (PWA)

  • A Progressive Web App (PWA) can be installed on a phone's home screen and can work offline.
  • It needs a web app manifest (name, icons, colours) and a service worker that caches files.
  • With Vite, the vite-plugin-pwa plugin creates both for you.
// vite.config.ts in a real project (npm install -D vite-plugin-pwa)
import { VitePWA } from "vite-plugin-pwa";

export default defineConfig({
  plugins: [
    vue(),
    VitePWA({
      registerType: "autoUpdate",
      manifest: { name: "Meera's Kitchen", short_name: "Kitchen", theme_color: "#2f9e6e" },
    }),
  ],
});
  • Test offline behaviour with DevTools → Application → Service workers. The lab cannot install a service worker.

TL;DR

  • onErrorCaptured = catches errors from child components. return false stops them.
  • Error boundary = a component that shows a fallback when its children fail.
  • app.config.errorHandler = the last stop for uncaught errors. Report them here.
  • Friendly states = loading, offline, server error, validation error, with a retry.
  • XSS = use {{ }}. v-html only for trusted or sanitised HTML.
  • Validate outside data = type guards or Zod. TypeScript types vanish at run time.
  • VITE_ variables = public. Secrets stay on the server.
  • Structure = views, components, composables, stores, services, router, utils.
  • Deploy = npm run build → dist/, base for sub-folders, SPA fallback to index.html.

Common mistakes

MistakeWrong ideaCorrect ideaWhat goes wrong
No boundary around risky widgetsOne error only breaks one partWrap them in an error boundaryThe whole page goes blank
Raw error text for usersDevelopers' messages are finePlain, kind words and a retryConfused or scared users
v-html with user inputIt is only formatting{{ }}, or sanitise firstStrangers run code on your page
Trusting API data shapesTypeScript checks itValidate at run timeCrashes on unexpected data
Secrets in VITE_ variables.env is privateKeep secrets on the serverAnyone can read them
Committing .env.localIt is part of the projectAdd it to .gitignorePrivate values leak into the repo
No SPA fallback on the hostDeep links work by themselvesSend every path to index.html404 on refresh
Wrong base for a sub-folderPaths fix themselvesSet base and the router's history baseBlank page, missing files

Check Yourself

Exercises

What to Remember

  • Wrap risky parts in error boundaries. Report the rest in errorHandler.
  • Show kind, specific error states with a way to retry.
  • Use {{ }} for user text, and validate data from outside the app.
  • VITE_ variables are public. Secrets stay on the server.
  • Build to dist/, set base if needed, and add the SPA fallback.

Next: Capstone: Build Meera's Kitchen