Examples
Real-world examples
Practical examples showing how to use safe in common application scenarios.
JSON parsing
import { safe } from '@cometloop/safe'
type ParseError = { type: 'PARSE_ERROR'; input: string; message: string }
function parseConfig(jsonString: string) {
const [config, error] = safe.sync(
() => JSON.parse(jsonString) as AppConfig,
(e): ParseError => ({
type: 'PARSE_ERROR',
input: jsonString.slice(0, 100),
message: e instanceof SyntaxError ? e.message : 'Unknown parse error',
})
)
if (error) {
console.error('Invalid config:', error.message)
return getDefaultConfig()
}
return config
}
API requests
import { safe } from '@cometloop/safe'
type ApiError = {
statusCode: number
message: string
endpoint: string
}
// 1. Define the normal function first
async function fetchJson<T>(
endpoint: string,
options?: RequestInit
): Promise<T> {
const response = await fetch(endpoint, options)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
return response.json()
}
// 2. Define the error mapper
const toApiError = (e: unknown): ApiError => ({
statusCode: 500,
message: e instanceof Error ? e.message : 'Network error',
endpoint: '',
})
// 3. Wrap it for safe usage
const safeFetch = safe.wrapAsync(fetchJson, toApiError)
// Usage in a service
async function getUsers() {
const [users, error] = await safeFetch<User[]>('/api/users')
if (error) {
if (error.statusCode === 401) {
redirectToLogin()
}
return []
}
return users
}
Form validation
import { safe } from '@cometloop/safe'
import { z } from 'zod'
const UserSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
age: z.number().min(18),
})
type ValidationError = {
field: string
message: string
}[]
// 1. Define the normal validation function
function parseUser(data: unknown) {
return UserSchema.parse(data)
}
// 2. Define the error mapper
const toValidationError = (e: unknown): ValidationError => {
if (e instanceof z.ZodError) {
return e.errors.map((err) => ({
field: err.path.join('.'),
message: err.message,
}))
}
return [{ field: 'unknown', message: 'Validation failed' }]
}
// 3. Wrap it for safe usage
const validateUser = safe.wrap(parseUser, toValidationError)
// Usage in form handler
function handleSubmit(formData: FormData) {
const [user, errors] = validateUser(Object.fromEntries(formData))
if (errors) {
errors.forEach((err) => setFieldError(err.field, err.message))
return
}
// user is fully validated and typed
createUser(user)
}
File operations
import { safe } from '@cometloop/safe'
import * as fs from 'fs/promises'
type FileError = {
operation: 'read' | 'write' | 'delete'
path: string
code: string
message: string
}
const toFileError = (e: unknown): FileError => ({
operation: 'read',
path: '',
code: (e as NodeJS.ErrnoException).code ?? 'UNKNOWN',
message: e instanceof Error ? e.message : 'File read failed',
})
const fileHooks = {
onSuccess: (content: Buffer | string, [path]: [string, ...unknown[]]) =>
console.log(
`Read ${path}: ${typeof content === 'string' ? content.length : content.byteLength} bytes`
),
onError: (error: FileError, [path]: [string, ...unknown[]]) =>
console.error(`Failed to read ${path}: ${error.code}`),
}
const safeReadFile = safe.wrapAsync(fs.readFile, toFileError, fileHooks)
// Usage
async function loadConfig(configPath: string) {
const [content, error] = await safeReadFile(configPath, 'utf-8')
if (error) {
if (error.code === 'ENOENT') {
return createDefaultConfig(configPath)
}
throw new Error(`Cannot load config: ${error.message}`)
}
return JSON.parse(content)
}
Database operations
import { safe } from '@cometloop/safe'
type DbError = {
code: 'NOT_FOUND' | 'DUPLICATE' | 'CONNECTION' | 'UNKNOWN'
table: string
message: string
}
function createDbErrorMapper(table: string) {
return (e: unknown): DbError => {
if (e instanceof Error) {
if (e.message.includes('unique constraint')) {
return { code: 'DUPLICATE', table, message: 'Record already exists' }
}
if (e.message.includes('not found')) {
return { code: 'NOT_FOUND', table, message: 'Record not found' }
}
}
return { code: 'UNKNOWN', table, message: String(e) }
}
}
class UserRepository {
private async createUser(data: CreateUserDto) {
return db.user.create({ data })
}
private async findUserById(id: string) {
const user = await db.user.findUnique({ where: { id } })
if (!user) throw new Error('not found')
return user
}
private toDbError = createDbErrorMapper('users')
private safeCreate = safe.wrapAsync(
this.createUser.bind(this),
this.toDbError
)
private safeFindById = safe.wrapAsync(
this.findUserById.bind(this),
this.toDbError
)
async create(data: CreateUserDto) {
const [user, error] = await this.safeCreate(data)
if (error) return { success: false, error }
return { success: true, data: user }
}
async findById(id: string) {
const [user, error] = await this.safeFindById(id)
if (error) return { success: false, error }
return { success: true, data: user }
}
}
Authentication
import { safe } from '@cometloop/safe'
import jwt from 'jsonwebtoken'
type AuthError = {
code: 'INVALID_TOKEN' | 'EXPIRED' | 'MISSING_CLAIMS'
message: string
}
function verifyJwt(token: string, secret: string): JwtPayload {
return jwt.verify(token, secret) as JwtPayload
}
const toAuthError = (e: unknown): AuthError => {
if (e instanceof jwt.TokenExpiredError) {
return { code: 'EXPIRED', message: 'Token has expired' }
}
if (e instanceof jwt.JsonWebTokenError) {
return { code: 'INVALID_TOKEN', message: e.message }
}
return { code: 'INVALID_TOKEN', message: 'Token verification failed' }
}
const authHooks = {
onSuccess: (payload: JwtPayload, [token]: [string, string]) => {
console.log(`Token verified for user: ${payload.sub}`)
},
onError: (error: AuthError, [token]: [string, string]) => {
console.warn(`Auth failed: ${error.code}`)
},
}
const verifyToken = safe.wrap(verifyJwt, toAuthError, authHooks)
// Middleware usage
function authMiddleware(req: Request, res: Response, next: NextFunction) {
const token = req.headers.authorization?.replace('Bearer ', '')
if (!token) {
return res.status(401).json({ error: 'No token provided' })
}
const [payload, error] = verifyToken(token, process.env.JWT_SECRET!)
if (error) {
const status = error.code === 'EXPIRED' ? 401 : 403
return res.status(status).json({ error: error.message })
}
req.user = payload
next()
}