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:
OffersWidgetcrashed, becauseofferswas missing. - The boundary caught it with
onErrorCaptured, showed a message, and the rest of the page stayed.
What You Will Learn
- How
onErrorCapturedand error boundaries work. - How errors travel up the component tree.
- A global handler with
app.config.errorHandler. - Friendly states for network, HTTP and validation errors.
- How to check data from outside the app.
- XSS,
v-htmland untrusted templates. - Why
VITE_variables are public, and where secrets belong. - Folder structure, linting, formatting,
.envfiles and modes. - 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
falseto stop the error from going further up. - Otherwise it keeps travelling up to the next boundary, then to
app.config.errorHandler. infosays 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.warnHandlercatches Vue warnings in development.- Errors outside Vue, such as in a
setTimeoutor an un-awaited Promise, never reach it. Catch those withwindow.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-htmlis 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
:hrefwithout checking it.javascript:links run code.
- What happened: the review was shown as text. No code ran.
- The
javascript:link was replaced with#, because onlyhttpandhttpswere allowed.
Validating Data from Outside
- Data from a server,
localStorageor 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
.envthat start withVITE_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
compilewould needunsafe-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
| Folder | Holds | Example |
|---|---|---|
src/views/ | Page components used by the router | MenuPage.vue |
src/components/ | Reusable parts | DishCard.vue, BaseButton.vue |
src/composables/ | use functions | useFetch.ts |
src/stores/ | Pinia stores | cart.ts |
src/services/ or src/api/ | API calls | dishes.ts |
src/router/ | Routes and guards | index.ts |
src/utils/ | Plain helper functions | money.ts |
src/assets/ | Images and CSS processed by Vite | logo.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-vuesets 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/essentialcatches errors only.flat/recommendedalso 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.developmentand.env.productionadd mode-specific values..env.local= your own machine only. Never commit it.import.meta.envgivesMODE,DEV,PROD,BASE_URLand everyVITE_variable.npm run devuses thedevelopmentmode.npm run buildusesproduction.
- 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 buildwrites optimised files todist/: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 previewservesdist/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/, setbaseinvite.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
/menudirectly, or refresh on it. - The host has no
menufile, 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-pwaplugin 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 falsestops 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-htmlonly 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/,basefor sub-folders, SPA fallback toindex.html.
Common mistakes
| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| No boundary around risky widgets | One error only breaks one part | Wrap them in an error boundary | The whole page goes blank |
| Raw error text for users | Developers' messages are fine | Plain, kind words and a retry | Confused or scared users |
v-html with user input | It is only formatting | {{ }}, or sanitise first | Strangers run code on your page |
| Trusting API data shapes | TypeScript checks it | Validate at run time | Crashes on unexpected data |
Secrets in VITE_ variables | .env is private | Keep secrets on the server | Anyone can read them |
Committing .env.local | It is part of the project | Add it to .gitignore | Private values leak into the repo |
| No SPA fallback on the host | Deep links work by themselves | Send every path to index.html | 404 on refresh |
Wrong base for a sub-folder | Paths fix themselves | Set base and the router's history base | Blank 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/, setbaseif needed, and add the SPA fallback.
Next: Capstone: Build Meera's Kitchen