Bunli uses the better-result library for type-safe error handling. This provides a consistent pattern for dealing with errors throughout your CLI.
TaggedError Pattern
Bunli uses the TaggedError pattern for defining custom error classes:
import { TaggedError } from '@bunli/core'
class DeploymentError extends TaggedError('DeploymentError')<{
message: string
environment: string
cause?: unknown
}>() {}
// Throw the error
throw new DeploymentError({
message: 'Deployment failed',
environment: 'production',
cause: originalError
})
TaggedError creates error classes with a tag property for pattern matching and better error discrimination.
Built-in Error Classes
Bunli provides several built-in error classes:
Core Errors
Thrown when option validation fails.import { BunliValidationError } from '@bunli/core'
throw new BunliValidationError(
'Invalid port number',
{
option: 'port',
value: 80,
command: 'start',
expectedType: 'number (1000-65535)',
hint: 'Use a port number between 1000 and 65535'
}
)
Properties:
option: string - Option name
value: unknown - Invalid value
command: string - Command name
expectedType: string - Expected type description
hint?: string - Helpful hint for the user
Thrown when configuration is invalid.import { InvalidConfigError } from '@bunli/core'
throw new InvalidConfigError({
message: 'Invalid configuration',
cause: zodError
})
Thrown when a command cannot be found.import { CommandNotFoundError } from '@bunli/core'
throw new CommandNotFoundError({
message: 'Command not found',
command: 'deploy'
})
Thrown when command execution fails.import { CommandExecutionError } from '@bunli/core'
throw new CommandExecutionError({
message: 'Command failed',
command: 'build',
cause: error
})
Thrown when an option fails validation.import { OptionValidationError } from '@bunli/core'
throw new OptionValidationError({
message: 'Invalid option value',
command: 'start',
option: 'port',
cause: error
})
Config Errors
Thrown when config file is not found.import { ConfigNotFoundError } from '@bunli/core'
throw new ConfigNotFoundError({
message: 'No config file found',
searched: ['bunli.config.ts', 'bunli.config.js']
})
Thrown when config file cannot be loaded.import { ConfigLoadError } from '@bunli/core'
throw new ConfigLoadError({
message: 'Failed to load config',
path: './bunli.config.ts',
cause: error
})
Plugin Errors
Thrown when a plugin fails to load.import { PluginLoadError } from '@bunli/core'
throw new PluginLoadError({
message: 'Failed to load plugin',
plugin: 'metrics-plugin',
cause: error
})
Thrown when plugin validation fails.import { PluginValidationError } from '@bunli/core'
throw new PluginValidationError({
message: 'Plugin validation failed',
plugin: 'auth-plugin'
})
Thrown when a plugin hook fails.import { PluginHookError } from '@bunli/core'
throw new PluginHookError({
message: 'Hook execution failed',
plugin: 'logging-plugin',
hook: 'beforeCommand',
cause: error
})
Result Type
The Result type represents operations that can fail:
import { Result, Ok, Err } from '@bunli/core'
function divide(a: number, b: number): Result<number, Error> {
if (b === 0) {
return Err(new Error('Division by zero'))
}
return Ok(a / b)
}
const result = divide(10, 2)
if (result.isOk()) {
console.log(`Result: ${result.value}`)
} else {
console.error(`Error: ${result.error.message}`)
}
Creating Results
import { Ok } from '@bunli/core'
function getUserName(): Result<string, Error> {
return Ok('Alice')
}
import { Err } from '@bunli/core'
function getUserName(): Result<string, Error> {
return Err(new Error('User not found'))
}
async function fetchUser(): Promise<Result<User, Error>> {
try {
const user = await api.getUser()
return Ok(user)
} catch (error) {
return Err(new Error('Failed to fetch user'))
}
}
Checking Results
const result = await loadConfigResult()
// Check if successful
if (result.isOk()) {
const config = result.value
console.log(`Loaded config: ${config.name}`)
}
// Check if failed
if (result.isErr()) {
const error = result.error
console.error(`Failed: ${error.message}`)
}
Error Matching
Match on specific error types:
import { matchError } from '@bunli/core'
import { ConfigNotFoundError, ConfigLoadError } from '@bunli/core'
const result = await loadConfigResult()
if (result.isErr()) {
matchError(result.error, {
ConfigNotFoundError: (err) => {
console.error('Config file not found')
console.error(`Searched: ${err.searched.join(', ')}`)
},
ConfigLoadError: (err) => {
console.error(`Failed to load: ${err.path}`)
console.error(err.cause)
},
_: (err) => {
console.error('Unknown error:', err)
}
})
}
The _ key matches any error not explicitly handled.
Creating Custom Errors
Define custom error classes for your CLI:
import { TaggedError } from '@bunli/core'
// Simple error
class NetworkError extends TaggedError('NetworkError')<{
message: string
url: string
}>() {}
// Error with optional fields
class DatabaseError extends TaggedError('DatabaseError')<{
message: string
query: string
cause?: unknown
}>() {}
// Error with nested data
class ValidationError extends TaggedError('ValidationError')<{
message: string
errors: Array<{ field: string; message: string }>
}>() {}
// Usage
throw new NetworkError({
message: 'Failed to fetch data',
url: 'https://api.example.com'
})
throw new ValidationError({
message: 'Validation failed',
errors: [
{ field: 'email', message: 'Invalid email format' },
{ field: 'age', message: 'Must be a positive number' }
]
})
Error Recovery
Handle errors gracefully in your commands:
Try-Catch
Result Type
Graceful Degradation
const deployCommand = defineCommand({
name: 'deploy',
handler: async ({ flags, spinner, colors }) => {
const spin = spinner('Deploying...')
try {
await deploy(flags.env)
spin.succeed('Deployment successful')
} catch (error) {
spin.fail('Deployment failed')
if (error instanceof DeploymentError) {
console.error(colors.red(`Failed to deploy to ${error.environment}`))
if (error.cause) {
console.error(colors.dim('Cause:'), error.cause)
}
} else {
console.error(colors.red('Unexpected error:'), error)
}
process.exit(1)
}
}
})
async function deployWithResult(
env: string
): Promise<Result<void, DeploymentError>> {
try {
await deploy(env)
return Ok(undefined)
} catch (error) {
return Err(new DeploymentError({
message: 'Deployment failed',
environment: env,
cause: error
}))
}
}
const deployCommand = defineCommand({
name: 'deploy',
handler: async ({ flags, spinner, colors }) => {
const result = await deployWithResult(flags.env)
if (result.isOk()) {
console.log(colors.green('Deployment successful'))
} else {
const error = result.error
console.error(colors.red(`Failed to deploy to ${error.environment}`))
process.exit(1)
}
}
})
const startCommand = defineCommand({
name: 'start',
handler: async ({ spinner, colors }) => {
const spin = spinner('Starting server...')
// Try to load config, fallback to defaults
const configResult = await loadConfigResult()
const config = configResult.isOk()
? configResult.value
: getDefaultConfig()
if (configResult.isErr()) {
console.warn(colors.yellow('Warning: Using default config'))
}
await startServer(config)
spin.succeed('Server started')
}
})
Validation Errors
Handle Zod validation errors:
import { defineCommand, option } from '@bunli/core'
import { z } from 'zod'
import { SchemaError } from '@bunli/core'
const command = defineCommand({
name: 'create',
options: {
email: option(
z.string().email(),
{ description: 'User email' }
)
},
handler: async ({ flags }) => {
// Email is already validated at this point
console.log(`Creating user: ${flags.email}`)
}
})
// Bunli automatically catches and formats validation errors:
// $ mycli create --email "invalid"
// Validation Error:
// email:
// • Invalid email
Error Helper Utilities
Convert any error to a string message.import { toErrorMessage } from '@bunli/core'
try {
await riskyOperation()
} catch (error) {
const message = toErrorMessage(error)
console.error(`Failed: ${message}`)
}
Best Practices
Use TaggedError for custom errors
TaggedError provides better error discrimination and pattern matching.class MyError extends TaggedError('MyError')<{
message: string
context?: unknown
}>() {}
Provide context in errors
Include relevant information to help debug issues.throw new DeploymentError({
message: 'Deploy failed',
environment: 'prod',
cause: originalError
})
Use Result type for expected errors
Return Result types for operations that commonly fail.function loadUser(): Result<User, NotFoundError>
Handle errors gracefully
Provide helpful error messages and recovery options.if (result.isErr()) {
console.error(colors.red('Error:'), result.error.message)
console.log(colors.dim('Hint: Try running with --verbose'))
}
See Also