Examples
createSafe examples
The createSafe factory is ideal when you need consistent error handling across a module, service, or entire application layer.
Application-wide error handling
Create a centralized error handling instance for your entire application:
import { createSafe } from '@cometloop/safe'
import * as Sentry from '@sentry/node'
type AppError = {
code: string
message: string
requestId?: string
stack?: string
}
export const appSafe = createSafe({
parseError: (e): AppError => {
const error = e instanceof Error ? e : new Error(String(e))
return {
code: error.name === 'Error' ? 'UNKNOWN_ERROR' : error.name,
message: error.message,
stack: process.env.NODE_ENV === 'development' ? error.stack : undefined,
}
},
defaultError: {
code: 'UNKNOWN_ERROR',
message: 'An unknown error occurred',
},
onSuccess: (result) => {
metrics.increment('operation.success')
},
onError: (error) => {
logger.error('Operation failed', {
code: error.code,
message: error.message,
})
Sentry.captureException(new Error(error.message), {
tags: { errorCode: error.code },
})
metrics.increment('operation.error', { code: error.code })
},
})
// Wrap functions for reuse throughout your application
const safeFindUser = appSafe.wrapAsync(userService.findById.bind(userService))
const safeJsonParse = appSafe.wrap(JSON.parse)
const [user, error] = await safeFindUser(id)
const [config, parseError] = safeJsonParse(configString)
API layer with logging
Create a dedicated safe instance for HTTP/API operations:
import { createSafe } from '@cometloop/safe'
type ApiError = {
type: 'NETWORK' | 'TIMEOUT' | 'AUTH' | 'SERVER' | 'UNKNOWN'
statusCode: number
message: string
endpoint: string
}
export const apiSafe = createSafe({
parseError: (e): ApiError => {
if (e instanceof TypeError && e.message.includes('fetch')) {
return {
type: 'NETWORK',
statusCode: 0,
message: 'Network unavailable',
endpoint: '',
}
}
if (e instanceof DOMException && e.name === 'AbortError') {
return {
type: 'TIMEOUT',
statusCode: 0,
message: 'Request timed out',
endpoint: '',
}
}
if (e instanceof Response) {
return {
type: e.status === 401 ? 'AUTH' : 'SERVER',
statusCode: e.status,
message: e.statusText,
endpoint: e.url,
}
}
return {
type: 'UNKNOWN',
statusCode: 500,
message: String(e),
endpoint: '',
}
},
defaultError: { type: 'UNKNOWN', statusCode: 500, message: 'Unknown error', endpoint: '' },
onSuccess: (result) => {
logger.debug('API request succeeded', { result })
},
onError: (error) => {
logger.warn('API request failed', {
type: error.type,
statusCode: error.statusCode,
endpoint: error.endpoint,
})
if (error.type === 'AUTH') {
authStore.clearSession()
}
},
})
async function fetchJson<T>(
endpoint: string,
options?: RequestInit
): Promise<T> {
const response = await fetch(endpoint, {
...options,
headers: {
'Content-Type': 'application/json',
...options?.headers,
},
})
if (!response.ok) {
throw response
}
return response.json()
}
const safeFetch = apiSafe.wrapAsync(fetchJson)
// Usage - all API calls get consistent error handling
export const api = {
users: {
list: () => safeFetch<User[]>('/api/users'),
get: (id: string) => safeFetch<User>(`/api/users/${id}`),
create: (data: CreateUser) =>
safeFetch<User>('/api/users', {
method: 'POST',
body: JSON.stringify(data),
}),
},
orders: {
list: () => safeFetch<Order[]>('/api/orders'),
submit: (data: OrderData) =>
safeFetch<Order>('/api/orders', {
method: 'POST',
body: JSON.stringify(data),
}),
},
}
// In components
const [users, error] = await api.users.list()
if (error) {
if (error.type === 'AUTH') showLoginModal()
else showToast(error.message)
}
Database layer
import { createSafe } from '@cometloop/safe'
import { db } from './drizzle'
type DbError = {
code: 'CONNECTION' | 'CONSTRAINT' | 'NOT_FOUND' | 'DUPLICATE' | 'QUERY' | 'UNKNOWN'
table?: string
message: string
}
export const dbSafe = createSafe({
parseError: (e): DbError => {
const message = e instanceof Error ? e.message : String(e)
if (message.includes('ECONNREFUSED') || message.includes('connection')) {
return { code: 'CONNECTION', message: 'Database connection failed' }
}
if (message.includes('unique constraint') || message.includes('duplicate key')) {
return { code: 'DUPLICATE', message: 'Record already exists' }
}
if (message.includes('foreign key constraint')) {
return { code: 'CONSTRAINT', message: 'Related record not found' }
}
if (message.includes('not found') || message.includes('no rows')) {
return { code: 'NOT_FOUND', message: 'Record not found' }
}
return { code: 'QUERY', message }
},
defaultError: { code: 'UNKNOWN', message: 'Database error' },
onSuccess: () => {
metrics.increment('db.query.success')
},
onError: (error) => {
metrics.increment('db.query.error', { code: error.code })
if (error.code === 'CONNECTION') {
alerting.critical('Database connection failed', error)
}
logger.error('Database operation failed', {
code: error.code,
table: error.table,
message: error.message,
})
},
})
export class UserRepository {
private async _findById(id: string) {
const user = await db.query.users.findFirst({
where: eq(users.id, id),
})
if (!user) throw new Error('not found')
return user
}
private async _create(data: CreateUserDto) {
const [user] = await db.insert(users).values(data).returning()
return user
}
findById = dbSafe.wrapAsync(this._findById.bind(this))
create = dbSafe.wrapAsync(this._create.bind(this))
}
Third-party integrations
Create dedicated instances for each external service:
import { createSafe } from '@cometloop/safe'
import Stripe from 'stripe'
type StripeError = {
code: 'CARD_DECLINED' | 'EXPIRED_CARD' | 'INVALID_REQUEST' | 'RATE_LIMIT' | 'API_ERROR'
message: string
declineCode?: string
retryable: boolean
}
export const stripeSafe = createSafe({
parseError: (e): StripeError => {
if (e instanceof Stripe.errors.StripeCardError) {
return {
code: 'CARD_DECLINED',
message: e.message,
declineCode: e.decline_code ?? undefined,
retryable: false,
}
}
if (e instanceof Stripe.errors.StripeRateLimitError) {
return {
code: 'RATE_LIMIT',
message: 'Too many requests to payment provider',
retryable: true,
}
}
if (e instanceof Stripe.errors.StripeAPIError) {
return {
code: 'API_ERROR',
message: 'Payment provider unavailable',
retryable: true,
}
}
return {
code: 'API_ERROR',
message: e instanceof Error ? e.message : 'Unknown payment error',
retryable: false,
}
},
defaultError: { code: 'API_ERROR', message: 'Unknown payment error', retryable: false },
onError: (error) => {
logger.error('Stripe operation failed', {
code: error.code,
declineCode: error.declineCode,
retryable: error.retryable,
})
metrics.increment('stripe.error', { code: error.code })
},
})
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
export const paymentService = {
createCustomer: stripeSafe.wrapAsync(
async (email: string, name: string) =>
stripe.customers.create({ email, name })
),
chargeCard: stripeSafe.wrapAsync(
async (customerId: string, amount: number, currency: string) =>
stripe.paymentIntents.create({
customer: customerId,
amount,
currency,
confirm: true,
}),
{
retry: {
times: 3,
waitBefore: (attempt) => attempt * 1000,
},
onRetry: (error, attempt, [customerId, amount, currency]) => {
if (error.retryable) {
logger.info(`Payment retry ${attempt} for customer ${customerId}`, {
amount,
currency,
errorCode: error.code,
})
}
},
}
),
}
Multi-tenant applications
Create safe instances dynamically with tenant-specific context:
import { createSafe, SafeInstance } from '@cometloop/safe'
type TenantError = {
code: string
message: string
tenantId: string
}
function createTenantSafe(tenantId: string): SafeInstance<TenantError> {
return createSafe({
parseError: (e): TenantError => ({
code: e instanceof Error ? e.name : 'UNKNOWN',
message: e instanceof Error ? e.message : String(e),
tenantId,
}),
defaultError: {
code: 'UNKNOWN',
message: 'Unknown error',
tenantId,
},
onSuccess: () => {
metrics.increment('tenant.operation.success', { tenantId })
},
onError: (error) => {
logger.error('Tenant operation failed', {
tenantId: error.tenantId,
code: error.code,
message: error.message,
})
metrics.increment('tenant.operation.error', {
tenantId: error.tenantId,
code: error.code,
})
},
})
}
class TenantContext {
private safe: SafeInstance<TenantError>
constructor(public readonly tenantId: string) {
this.safe = createTenantSafe(tenantId)
}
private async _getUsers() {
return db.users.findMany({ where: { tenantId: this.tenantId } })
}
private async _createDocument(data: CreateDocumentDto) {
return db.documents.create({
data: { ...data, tenantId: this.tenantId },
})
}
getUsers = this.safe.wrapAsync(this._getUsers.bind(this))
createDocument = this.safe.wrapAsync(this._createDocument.bind(this))
}
// Middleware creates tenant context per request
app.get('/api/users', async (req, res) => {
const [users, error] = await req.tenant.getUsers()
if (error) {
// error.tenantId is automatically set
return res.status(500).json({
error: error.message,
code: error.code,
})
}
res.json(users)
})