Unified Error Handling
Comprehensive guide to the SveltyCMS unified error handling system including AppError, apiHandler, apiClient, and validation integration.
On this page
SveltyCMS implements a comprehensive error handling system that provides consistent error responses across the entire API surface, integrates with client-side validation, and enables robust debugging in development.
Overview
The unified error handling system consists of three core modules:
| Module | Path | Purpose |
|---|---|---|
| error-handling.ts | src/utils/error-handling.ts |
Core error types, handlers, and utilities |
| api-handler.ts | src/utils/api-handler.ts |
Higher-order function for API route protection |
| api.ts | src/utils/api.ts |
Client-side fetch wrapper with error handling |
Core Concepts
ApiErrorResponse Interface
All API errors return a standardized response shape:
interface ApiErrorResponse {
success: false;
message: string; // Human-readable error message
code?: string; // Machine-readable error code
issues?: string[]; // Validation or field-specific issues
status?: number; // HTTP status code (for logging)
stack?: string; // Stack trace (development only)
}
AppError Class
The AppError class is the standard way to throw errors in the application:
import { AppError } from "@utils/error-handling";
// Basic usage
throw new AppError("User not found", 404, "NOT_FOUND");
// With details
throw new AppError("Validation failed", 400, "VALIDATION_ERROR", { fields: ["email", "password"] });
Constructor Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
message |
string |
required | Human-readable error message |
status |
number |
500 |
HTTP status code |
code |
string |
'INTERNAL_ERROR' |
Machine-readable error code |
details |
unknown |
undefined |
Additional error context |
Error Codes
Standard error codes used across the application:
| Code | Status | Description |
|---|---|---|
VALIDATION_ERROR |
400 | Invalid input data |
UNAUTHORIZED |
401 | Authentication required |
FORBIDDEN |
403 | Insufficient permissions |
NOT_FOUND |
404 | Resource not found |
CONFLICT |
409 | Resource already exists |
INTERNAL_ERROR |
500 | Server error |
NETWORK_ERROR |
- | Client-side network failure |
Server-Side Usage
Unified Gatekeeper Handlers
In the new architecture, all API logic is encapsulated in handlers within src/routes/api/[...path]/handlers/.
// src/routes/api/[...path]/handlers/user.ts
import { json } from "@sveltejs/kit";
import { AppError } from "@utils/error-handling";
export async function handleUserRoutes(event, cms, segments) {
const userId = segments[1];
if (!userId) {
throw new AppError("User ID is required", 400, "MISSING_USER_ID");
}
const user = await cms.users.findById(userId);
if (!user) {
throw new AppError("User not found", 404, "NOT_FOUND");
}
return json({ success: true, data: user });
}
Handling Different Error Types
The handleApiError function handles various error types:
// Valibot validation errors
import { parse } from "valibot";
const data = parse(userSchema, await event.request.json());
// ➜ ValiError is caught and formatted as ApiErrorResponse
// SvelteKit HttpErrors
import { error } from "@sveltejs/kit";
throw error(403, "Access denied");
// ➜ Converted to ApiErrorResponse with FORBIDDEN code
// Custom AppErrors
throw new AppError("Custom message", 400, "CUSTOM_CODE");
// ➜ Directly formatted as ApiErrorResponse
// Unknown errors
throw new Error("Something went wrong");
// ➜ Wrapped with INTERNAL_ERROR code
Client-Side Usage
The fetchApi Function
Use fetchApi for all API calls to get consistent error handling:
import { fetchApi } from "@utils/api";
// GET request
const response = await fetchApi<User[]>("/api/users");
if (response.success) {
console.log("Users:", response.data);
} else {
console.error("Error:", response.message);
console.error("Code:", response.code);
}
ApiResponse Interface
interface ApiResponse<T> {
success: boolean;
data?: T; // Present when success: true
message?: string; // Error message when success: false
code?: string; // Error code when success: false
issues?: string[]; // Validation issues when applicable
}
Making Requests
// POST request with body
const result = await fetchApi<User>("/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "John", email: "john@example.com" }),
});
// PUT request
const updateResult = await fetchApi<User>(`/api/users/${id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(userData),
});
// DELETE request
const deleteResult = await fetchApi(`/api/users/${id}`, {
method: "DELETE",
});
Error Handling Patterns
async function createUser(data: UserInput) {
const response = await fetchApi<User>("/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
if (!response.success) {
// Check error code for specific handling
switch (response.code) {
case "VALIDATION_ERROR":
// Show validation errors
response.issues?.forEach((issue) => showFieldError(issue));
break;
case "CONFLICT":
showToast("User already exists", "warning");
break;
case "NETWORK_ERROR":
showToast("Please check your connection", "error");
break;
default:
showToast(response.message || "An error occurred", "error");
}
return null;
}
return response.data;
}
Validation Integration
Widget Validation
Widgets integrate with the validationStore for field-level error handling. The handleWidgetValidation helper standardizes this process:
// In widget input.svelte
import { handleWidgetValidation } from "@widgets/widgetErrorHandler";
import { validationStore } from "@stores/store.svelte";
import { parse } from "valibot";
async function validateInput() {
const result = handleWidgetValidation(() => parse(validationSchema, currentValue), {
fieldName,
updateStore: true,
isTouched,
});
// Result contains { valid, message, issues }
return result.valid;
}
ValidationStore API
// Set an error
validationStore.setError("email", "Invalid email format");
// Clear an error
validationStore.clearError("email");
// Clear all errors
validationStore.clearAllErrors();
// Check if valid
if (validationStore.isValid) {
// All fields are valid
}
// Get specific error
const emailError = validationStore.getError("email");
Utility Functions
getErrorMessage
Extract a human-readable message from any error:
import { getErrorMessage } from "@utils/error-handling";
try {
await riskyOperation();
} catch (error) {
const message = getErrorMessage(error);
console.error("Operation failed:", message);
}
isAppError
Type guard to check if an error is an AppError:
import { isAppError } from "@utils/error-handling";
if (isAppError(error)) {
console.log("Status:", error.status);
console.log("Code:", error.code);
}
Testing
Unit Testing Error Handling
import { describe, test, expect } from "vitest";
import { AppError, handleApiError, getErrorMessage } from "@utils/error-handling";
describe("AppError", () => {
test("creates error with correct properties", () => {
const error = new AppError("Test error", 400, "TEST_CODE");
expect(error.message).toBe("Test error");
expect(error.status).toBe(400);
expect(error.code).toBe("TEST_CODE");
expect(error.name).toBe("AppError");
});
});
describe("getErrorMessage", () => {
test("extracts message from AppError", () => {
const error = new AppError("Custom message", 400);
expect(getErrorMessage(error)).toBe("Custom message");
});
test("extracts message from standard Error", () => {
const error = new Error("Standard error");
expect(getErrorMessage(error)).toBe("Standard error");
});
test("returns default for unknown types", () => {
expect(getErrorMessage("string error")).toBe("string error");
expect(getErrorMessage(null)).toBe("An unknown error occurred");
});
});
Integration Testing API Endpoints
import { describe, test, expect } from "vitest";
describe("API Error Handling", () => {
test("returns validation error for invalid input", async () => {
const response = await fetch("/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: "invalid" }),
});
const data = await response.json();
expect(response.status).toBe(400);
expect(data.success).toBe(false);
expect(data.code).toBe("VALIDATION_ERROR");
expect(data.issues).toBeDefined();
});
test("returns 404 for non-existent resource", async () => {
const response = await fetch("/api/users/non-existent-id");
const data = await response.json();
expect(response.status).toBe(404);
expect(data.success).toBe(false);
expect(data.code).toBe("NOT_FOUND");
});
});
Running Tests with Bun
# Run all tests
bun test
# Run error handling tests
bun test --grep "error"
# Run with coverage
bun test --coverage
Best Practices
Do’s
- Always use apiHandler for API routes
- Use specific error codes for different failure modes
- Include helpful error messages for users
- Log with context in development
- Validate on both client and server
- Use the type guards for safe error checking
Don’ts
- Don’t expose stack traces in production
- Don’t catch and swallow errors silently
- Don’t use generic error messages when specific ones are better
- Don’t bypass apiHandler in API routes
- Don’t store sensitive data in error details
Error Message Guidelines
// ❌ Bad: Too technical
throw new AppError("ECONNREFUSED: Connection refused", 500);
// ✅ Good: User-friendly
throw new AppError(
"Unable to connect to the database. Please try again later.",
500,
"DB_CONNECTION_ERROR",
);
// ❌ Bad: Too vague
throw new AppError("Invalid input", 400);
// ✅ Good: Specific
throw new AppError("Email address is not valid", 400, "INVALID_EMAIL");
Troubleshooting
Common Issues
Stack traces showing in production
- Check
NODE_ENVis set toproduction - Verify
devflag is not hardcoded
Validation errors not formatted correctly
- Ensure using Valibot schema with
parse() - Check
handleApiErrorreceives the ValiError
CORS errors masking API errors
- Check CORS headers are configured
- Verify preflight requests are handled
Network errors not caught
- Ensure using
fetchApiwrapper - Check for browser extensions blocking requests
Related Documentation
- API Documentation - API endpoint reference
- Widget Development Guide - Widget validation patterns
- Testing Guide - Testing strategies
- Troubleshooting - Common issues and solutions
API Reference
Exports from error-handling.ts
// Classes
export class AppError extends Error { ... }
// Functions
export function handleApiError(err: unknown, event: RequestEvent): Response;
export function getErrorMessage(err: unknown): string;
export function isAppError(err: unknown): err is AppError;
export function isHttpError(err: unknown): err is HttpError;
// Types
export interface ApiErrorResponse { ... }
Exports from apiHandler.ts
export function apiHandler(handler: ApiHandlerCallback): RequestHandler;
Exports from apiClient.ts
export function fetchApi<T>(endpoint: string, options?: RequestInit): Promise<ApiResponse<T>>;
export interface ApiResponse<T> { ... }
Operational Debugging
Structured error codes enable fast log filtering in production:
# Before: grep for generic message strings
cat production.log | grep "User creation failed"
# After: filter by structured code
cat production.log | grep USER_CREATE_FAILED
Each raise() call auto-generates semantic codes (NOT_FOUND, UNAUTHORIZED, etc.) or accepts custom codes (USER_CREATE_FAILED). The handleError hook attaches userId, tenantId, and request path to every error log.