Error Handling
Stacks provides a comprehensive error handling system built on ts-error-handling, featuring Result types for functional error handling, custom exceptions, and error reporting.
Overview
The Stacks error handling system helps you:
- Handle errors gracefully - Without crashing your application
- Use Result types - Explicit, type-safe error handling
- Create custom exceptions - Domain-specific error classes
- Report errors - Send errors to monitoring services
Quick Start
Basic Error Handling
import { handleError } from '@stacksjs/error-handling'
try {
await riskyOperation()
} catch (error) {
handleError(error)
}
With Options
handleError(error, {
shouldExit: false, // Don't exit process
silent: false, // Log to console
message: 'Custom error context',
})
Result Types
Use Result types for functional error handling, avoiding try-catch blocks.
Basic Usage
import { err, ok, type Result } from '@stacksjs/error-handling'
function divide(a: number, b: number): Result<number, string> {
if (b === 0) {
return err('Cannot divide by zero')
}
return ok(a / b)
}
// Usage
const result = divide(10, 2)
if (result.isOk()) {
console.log('Result:', result.value) // 5
} else {
console.log('Error:', result.error)
}
Async Results
import { err, ok, type Result } from '@stacksjs/error-handling'
async function fetchUser(id: number): Promise<Result<User, Error>> {
try {
const response = await fetch(`/api/users/${id}`)
if (!response.ok) {
return err(new Error(`HTTP ${response.status}`))
}
const user = await response.json()
return ok(user)
} catch (error) {
return err(error as Error)
}
}
// Usage
const result = await fetchUser(1)
if (result.isOk()) {
console.log('User:', result.value.name)
}
Chaining Results
function validateEmail(email: string): Result<string, string> {
if (!email.includes('@')) {
return err('Invalid email format')
}
return ok(email.toLowerCase())
}
function validatePassword(password: string): Result<string, string> {
if (password.length < 8) {
return err('Password must be at least 8 characters')
}
return ok(password)
}
// Chain validations
const result = validateEmail(email)
.andThen(() => validatePassword(password))
.map(data => ({ ...data, validated: true }))
Custom Exceptions
Creating Custom Exceptions
// app/Exceptions/ValidationException.ts
export class ValidationException extends Error {
public readonly errors: Record<string, string[]>
constructor(errors: Record<string, string[]>) {
const message = Object.values(errors).flat().join(', ')
super(message)
this.name = 'ValidationException'
this.errors = errors
}
static fromField(field: string, message: string): ValidationException {
return new ValidationException({ [field]: [message] })
}
}
HTTP Exceptions
// app/Exceptions/HttpException.ts
export class HttpException extends Error {
public readonly statusCode: number
constructor(statusCode: number, message: string) {
super(message)
this.name = 'HttpException'
this.statusCode = statusCode
}
static badRequest(message = 'Bad Request') {
return new HttpException(400, message)
}
static unauthorized(message = 'Unauthorized') {
return new HttpException(401, message)
}
static notFound(message = 'Not Found') {
return new HttpException(404, message)
}
static serverError(message = 'Internal Server Error') {
return new HttpException(500, message)
}
}
Using Custom Exceptions
import { ValidationException } from '@/Exceptions/ValidationException'
import { HttpException } from '@/Exceptions/HttpException'
async function createUser(data: CreateUserInput) {
// Validation
const errors: Record<string, string[]> = {}
if (!data.email) {
errors.email = ['Email is required']
}
if (!data.password || data.password.length < 8) {
errors.password = ['Password must be at least 8 characters']
}
if (Object.keys(errors).length > 0) {
throw new ValidationException(errors)
}
// Check for duplicate
const existing = await User.findByEmail(data.email)
if (existing) {
throw HttpException.badRequest('Email already exists')
}
return User.create(data)
}
Error Pages
Custom Error Pages
Create custom error pages in resources/layouts/:
<!-- resources/layouts/404.stx -->
<template
<divclass"error-page"
<h1404</h1
<h2Page Not Found</h2
<pThe page you're looking for doesn't exist.</p
<ahref"/"Go back home</a
</div
</template
<!-- resources/layouts/500.stx -->
<template
<divclass"error-page"
<h1500</h1
<h2Something went wrong</h2
<pWe're sorry, but something went wrong on our end.</p
<buttononclick"location.reload()"Try Again</button
</div
</template
Global Error Handler
HTTP Error Handler
// app/Middleware/ErrorHandler.ts
import { HttpException } from '@/Exceptions/HttpException'
import { ValidationException } from '@/Exceptions/ValidationException'
import { handleError } from '@stacksjs/error-handling'
export async function errorHandler(error: Error, request: Request) {
// Log the error
handleError(error, { shouldExit: false })
// HTTP exceptions
if (error instanceof HttpException) {
return Response.json(
{ error: error.message },
{ status: error.statusCode }
)
}
// Validation exceptions
if (error instanceof ValidationException) {
return Response.json(
{ error: 'Validation failed', errors: error.errors },
{ status: 422 }
)
}
// Generic server error
return Response.json(
{ error: 'Internal Server Error' },
{ status: 500 }
)
}
Error Reporting
Logging Errors
import { handleError } from '@stacksjs/error-handling'
import { log } from '@stacksjs/logging'
try {
await riskyOperation()
} catch (error) {
// Log with context
log.error('Operation failed', {
error: error.message,
stack: error.stack,
userId: currentUser?.id,
})
// Handle (may exit process)
handleError(error, { shouldExit: false })
}
External Error Reporting
// app/Services/ErrorReporter.ts
class ErrorReporter {
static async report(error: Error, context?: Record<string, unknown>) {
// Send to Sentry, Bugsnag, etc.
if (process.env.SENTRY_DSN) {
Sentry.captureException(error, { extra: context })
}
// Send to Slack
if (process.env.SLACK_WEBHOOK_URL) {
await fetch(process.env.SLACK_WEBHOOK_URL, {
method: 'POST',
body: JSON.stringify({
text: `Error: ${error.message}`,
attachments: [{ color: 'danger', text: error.stack }],
}),
})
}
}
}
Best Practices
Use Result Types for Expected Failures
// Good: Explicit error handling
async function findUser(id: number): Promise<Result<User, string>> {
const user = await db.users.find(id)
if (!user) {
return err('User not found')
}
return ok(user)
}
Use Exceptions for Unexpected Failures
// Good: Exception for unexpected error
async function processPayment(order: Order) {
const gateway = getPaymentGateway()
if (!gateway) {
throw new Error('Payment gateway not configured')
}
return gateway.charge(order)
}
Always Add Context
try {
await processOrder(orderId)
} catch (error) {
handleError(error, {
message: `Failed to process order ${orderId}`,
})
}
Related Resources
Underlying Libraries
- ts-error-handling - Result types and error utilities
Related Stacks Packages
- Logging Package - Error logging
- Validation Package - Input validation errors
Related Guides
- Logging - Logging errors
- Validation - Handling validation errors