Error Handling
Lucid Resourceful provides a comprehensive error handling system with structured exceptions for different types of failures. All errors extend the AdonisJS Exception class and include detailed error information, HTTP status codes, and type-safe error checking capabilities.
Overview
The error system in Lucid Resourceful is designed to provide:
- Structured Error Information: All errors include error codes, HTTP status codes, and detailed messages
- Type-Safe Error Checking: Use type guards to identify specific error types
- Rich Context: Validation errors include Joi validation details
- Consistent HTTP Status Codes: Proper status codes for different error scenarios
- Developer-Friendly Messages: Clear, actionable error messages for debugging
Importing Errors
Errors can be imported in two ways:
From the Root Module
import { errors } from '@nhtio/lucid-resourceful'
// Use specific error classes
const { E_FORBIDDEN, E_INVALID_PAYLOAD_EXCEPTION } = errors
try {
// Some operation that might fail
} catch (error) {
if (error instanceof errors.E_FORBIDDEN) {
// Handle forbidden access
}
}From the Errors Sub-Module
import {
E_FORBIDDEN,
E_INVALID_PAYLOAD_EXCEPTION,
E_RECORD_NOT_FOUND_EXCEPTION
} from '@nhtio/lucid-resourceful/errors'
try {
// Some operation that might fail
} catch (error) {
if (error instanceof E_FORBIDDEN) {
// Handle forbidden access
}
}Error Categories
Lucid Resourceful errors are organized into several categories based on their context and purpose:
Configuration and Setup Errors
Errors that occur during model setup, decorator configuration, or data type instantiation.
Validation Errors
Errors related to data validation, both at the model level and request payload level.
Access Control Errors
Errors thrown when access control restrictions prevent operations.
Query and Database Errors
Errors related to database queries, Lucene query parsing, and missing records.
Type Casting Errors
Errors thrown during data type conversion and casting operations.
Error Types
Configuration Errors
E_INVALID_RESOURCEFUL_DATA_TYPE_OPTIONS
Thrown when invalid options are provided to data type constructors.
import { E_INVALID_RESOURCEFUL_DATA_TYPE_OPTIONS } from '@nhtio/lucid-resourceful/errors'
import { ResourcefulStringType } from '@nhtio/lucid-resourceful/definitions'
try {
const stringType = new ResourcefulStringType({
minLength: -1, // Invalid: negative length
maxLength: 5
})
} catch (error) {
if (error instanceof E_INVALID_RESOURCEFUL_DATA_TYPE_OPTIONS) {
console.error('Invalid data type options:', error.message)
console.error('Validation details:', error.details)
// Error includes Joi validation details
}
}Properties:
status: 500code:'E_INVALID_RESOURCEFUL_DATA_TYPE_OPTIONS'details: Joi validation error details (if available)
E_INVALID_RESOURCEFUL_MIXIN_OPTIONS
Thrown when invalid options are provided to the withResourceful mixin.
import { E_INVALID_RESOURCEFUL_MIXIN_OPTIONS } from '@nhtio/lucid-resourceful/errors'
import { withResourceful } from '@nhtio/lucid-resourceful'
try {
class User extends withResourceful({
name: 123, // Invalid: name must be string
accessControlFilters: 'invalid' // Invalid: must be object
})(BaseModel) {}
} catch (error) {
if (error instanceof E_INVALID_RESOURCEFUL_MIXIN_OPTIONS) {
console.error('Invalid mixin options:', error.message)
console.error('Validation details:', error.details)
}
}Properties:
status: 500code:'E_INVALID_RESOURCEFUL_MIXIN_OPTIONS'details: Joi validation error details (if available)
E_INVALID_RESOURCEFUL_DECORATOR_OPTIONS
Thrown when invalid options are provided to resourceful decorators.
import { E_INVALID_RESOURCEFUL_DECORATOR_OPTIONS } from '@nhtio/lucid-resourceful/errors'
import { resourcefulColumn } from '@nhtio/lucid-resourceful'
try {
class User extends BaseModel {
@resourcefulColumn({
type: 'invalid-type', // Invalid data type
validate: 'not-a-schema' // Invalid validation schema
})
declare name: string
}
} catch (error) {
if (error instanceof E_INVALID_RESOURCEFUL_DECORATOR_OPTIONS) {
console.error(`Invalid decorator options for field "${error.fieldName}":`, error.message)
console.error('Validation details:', error.details)
}
}Properties:
status: 500code:'E_INVALID_RESOURCEFUL_DECORATOR_OPTIONS'details: Joi validation error details (if available)
Validation Errors
E_INVALID_PAYLOAD_EXCEPTION
Thrown when request payload validation fails during create or update operations.
import { E_INVALID_PAYLOAD_EXCEPTION } from '@nhtio/lucid-resourceful/errors'
try {
const user = await User.$onResourcefulCreate({
name: '', // Invalid: empty name
email: 'invalid-email' // Invalid: not a valid email format
}, ctx, app)
} catch (error) {
if (error instanceof E_INVALID_PAYLOAD_EXCEPTION) {
console.error('Payload validation failed:', error.message)
console.error('Validation details:', error.details)
// Return structured error response
return response.status(error.status).json({
error: error.code,
message: error.message,
details: error.details
})
}
}Properties:
status: 422code:'E_INVALID_PAYLOAD_EXCEPTION'details: Joi validation error details (if available)
E_FORBIDDEN_PAYLOAD_EXCEPTION
Thrown when payload validation fails due to authorization constraints.
import { E_FORBIDDEN_PAYLOAD_EXCEPTION } from '@nhtio/lucid-resourceful/errors'
try {
const user = await User.$onResourcefulCreate({
name: 'John Doe',
role: 'admin' // Forbidden: regular users cannot set admin role
}, ctx, app)
} catch (error) {
if (error instanceof E_FORBIDDEN_PAYLOAD_EXCEPTION) {
console.error('Payload authorization failed:', error.message)
console.error('Validation details:', error.details)
return response.status(error.status).json({
error: error.code,
message: 'You are not authorized to perform this action'
})
}
}Properties:
status: 403code:'E_FORBIDDEN_PAYLOAD_EXCEPTION'details: Joi validation error details (if available)
E_INVALID_RESOUREFUL_INDEX_REQUEST_EXCEPTION
Thrown when request parameters for index/list operations fail validation.
import { E_INVALID_RESOUREFUL_INDEX_REQUEST_EXCEPTION } from '@nhtio/lucid-resourceful/errors'
try {
const result = await User.$onResourcefulIndex(
'invalid-lucene-query[]', // Invalid Lucene syntax
-1, // Invalid page number
0, // Invalid perPage value
['nonexistent_field'], // Invalid field name
ctx,
app
)
} catch (error) {
if (error instanceof E_INVALID_RESOUREFUL_INDEX_REQUEST_EXCEPTION) {
console.error('Index request validation failed:', error.message)
console.error('Validation details:', error.details)
}
}Properties:
status: 422code:'E_INVALID_RESOUREFUL_INDEX_REQUEST_EXCEPTION'details: Joi validation error details (if available)
Access Control Errors
E_FORBIDDEN
Thrown when access control filters deny access to operations.
import { E_FORBIDDEN } from '@nhtio/lucid-resourceful/errors'
try {
const user = await User.$onResourcefulRead(123, ctx, app)
} catch (error) {
if (error instanceof E_FORBIDDEN) {
console.error('Access denied:', error.message)
return response.status(error.status).json({
error: error.code,
message: 'You do not have permission to access this resource'
})
}
}Properties:
status: 403code:'E_FORBIDDEN'details: Error message string
E_INVALID_COLUMN_ACCESS
Thrown when trying to access columns that don't exist or are not accessible.
import { E_INVALID_COLUMN_ACCESS } from '@nhtio/lucid-resourceful/errors'
try {
const result = await User.$onResourcefulIndex(
'name:john',
1,
10,
['password', 'secret_key'], // Fields not accessible
ctx,
app
)
} catch (error) {
if (error instanceof E_INVALID_COLUMN_ACCESS) {
console.error('Column access denied:', error.message)
// Might be: "You don't have access to column "password" or the column does not exist."
}
}Properties:
status: 403code:'E_INVALID_COLUMN_ACCESS'details: Error message string
Query and Database Errors
E_RECORD_NOT_FOUND_EXCEPTION
Thrown when a requested record cannot be found or accessed.
import { E_RECORD_NOT_FOUND_EXCEPTION } from '@nhtio/lucid-resourceful/errors'
try {
const user = await User.$onResourcefulRead(99999, ctx, app) // Non-existent ID
} catch (error) {
if (error instanceof E_RECORD_NOT_FOUND_EXCEPTION) {
console.error('Record not found:', error.message)
return response.status(error.status).json({
error: error.code,
message: 'The requested resource was not found'
})
}
}Properties:
status: 404code:'E_RECORD_NOT_FOUND_EXCEPTION'
E_MISSING_PRIMARY_KEY_EXCEPTION
Thrown when operations require a primary key but the model doesn't have one defined.
import { E_MISSING_PRIMARY_KEY_EXCEPTION } from '@nhtio/lucid-resourceful/errors'
try {
// Model without a primary key
class Settings extends withResourceful()(BaseModel) {
// No primary key defined
}
const result = await Settings.$onResourcefulRead(1, ctx, app)
} catch (error) {
if (error instanceof E_MISSING_PRIMARY_KEY_EXCEPTION) {
console.error('Model configuration error:', error.message)
// Message: "Settings" does not have a primary key defined
}
}Properties:
status: 503code:'E_MISSING_PRIMARY_KEY_EXCEPTION'
E_INVALID_LUCENE_QUERY
Thrown when Lucene query syntax is invalid.
import { E_INVALID_LUCENE_QUERY } from '@nhtio/lucid-resourceful/errors'
try {
const result = await User.$onResourcefulIndex(
'name:[invalid syntax', // Unclosed bracket
1,
10,
['id', 'name'],
ctx,
app
)
} catch (error) {
if (error instanceof E_INVALID_LUCENE_QUERY) {
console.error('Invalid query syntax:', error.message)
// Provide user-friendly feedback about query syntax
}
}Properties:
status: 400code:'E_INVALID_LUCENE_QUERY'details: Error message string
E_LUCENE_SYNTAX_EXCEPTION
Thrown when Lucene query parsing encounters syntax errors with detailed location information.
import { E_LUCENE_SYNTAX_EXCEPTION } from '@nhtio/lucid-resourceful/errors'
try {
const result = await User.$onResourcefulIndex(
'name:john AND (email:', // Unclosed parenthesis
1,
10,
['id', 'name'],
ctx,
app
)
} catch (error) {
if (error instanceof E_LUCENE_SYNTAX_EXCEPTION) {
console.error('Query syntax error:', error.message)
console.error(`Error at line ${error.line}, column ${error.column}, offset ${error.offset}`)
// Provide precise error location for debugging
return response.status(error.status).json({
error: error.code,
message: 'Invalid query syntax',
location: {
line: error.line,
column: error.column,
offset: error.offset
}
})
}
}Properties:
status: 422code:'E_LUCENE_SYNTAX_EXCEPTION'offset: Error offset in the query stringline: Line number where error occurredcolumn: Column number where error occurred
E_UNEXPECTED_COLUMN_IN_QUERY
Thrown when Lucene queries reference columns that don't exist in the model.
import { E_UNEXPECTED_COLUMN_IN_QUERY } from '@nhtio/lucid-resourceful/errors'
try {
const result = await User.$onResourcefulIndex(
'nonexistent_field:value', // Field doesn't exist
1,
10,
['id', 'name'],
ctx,
app
)
} catch (error) {
if (error instanceof E_UNEXPECTED_COLUMN_IN_QUERY) {
console.error('Unknown column in query:', error.message)
// Message: Unexpected column in query: "nonexistent_field"
}
}Properties:
status: 400code:'E_UNEXPECTED_COLUMN_IN_QUERY'details: Error message string
Type Casting Errors
E_UNCASTABLE
Thrown when values cannot be cast to the expected data type.
import { E_UNCASTABLE } from '@nhtio/lucid-resourceful/errors'
try {
// This error typically occurs internally during data processing
const value = castValueAsNumber('not-a-number')
} catch (error) {
if (error instanceof Error && error.code === 'E_UNCASTABLE') {
console.error('Type casting failed:', error.message)
// Message: The value could not be cast to a valid number
}
}Properties:
status: 500code:'E_UNCASTABLE'
E_INVALID_PREPARED_VALUE
Thrown when values fail preparation for database storage.
import { E_INVALID_PREPARED_VALUE } from '@nhtio/lucid-resourceful/errors'
try {
// This error typically occurs during model save operations
user.email = 'invalid-email-format'
await user.save()
} catch (error) {
if (error instanceof Error && error.code === 'E_INVALID_PREPARED_VALUE') {
console.error('Value preparation failed:', error.message)
// Message: The value for "email" is not a valid email
}
}Properties:
status: 422code:'E_INVALID_PREPARED_VALUE'
E_INVALID_CONSUMED_VALUE
Thrown when values cannot be consumed from database results.
import { E_INVALID_CONSUMED_VALUE } from '@nhtio/lucid-resourceful/errors'
try {
// This error typically occurs during model hydration
const user = await User.find(1)
} catch (error) {
if (error instanceof Error && error.code === 'E_INVALID_CONSUMED_VALUE') {
console.error('Value consumption failed:', error.message)
// Message: The value for "birth_date" could not be consumed as a valid date
}
}Properties:
status: 500code:'E_INVALID_CONSUMED_VALUE'
Type Guards for Error Checking
Lucid Resourceful provides type guards to safely check error types and access their properties.
Using isInstanceOf Type Guard
The isInstanceOf type guard provides a safe way to check error types, especially useful when dealing with bundled or transformed code where instanceof might not work reliably.
import { guards } from '@nhtio/lucid-resourceful/utils'
import {
E_FORBIDDEN,
E_INVALID_PAYLOAD_EXCEPTION,
E_RECORD_NOT_FOUND_EXCEPTION
} from '@nhtio/lucid-resourceful/errors'
try {
const user = await User.$onResourcefulCreate(payload, ctx, app)
} catch (error) {
// Using isInstanceOf for reliable type checking
if (guards.isInstanceOf(error, 'E_FORBIDDEN', E_FORBIDDEN)) {
return response.status(403).json({
error: 'FORBIDDEN',
message: 'Access denied'
})
}
if (guards.isInstanceOf(error, 'E_INVALID_PAYLOAD_EXCEPTION', E_INVALID_PAYLOAD_EXCEPTION)) {
return response.status(422).json({
error: 'VALIDATION_ERROR',
message: 'Invalid input data',
details: error.details
})
}
if (guards.isInstanceOf(error, 'E_RECORD_NOT_FOUND_EXCEPTION', E_RECORD_NOT_FOUND_EXCEPTION)) {
return response.status(404).json({
error: 'NOT_FOUND',
message: 'Resource not found'
})
}
// Unknown error - re-throw
throw error
}Comprehensive Error Handler
Here's an example of a comprehensive error handler that handles all Lucid Resourceful error types:
import { guards } from '@nhtio/lucid-resourceful/utils'
import { errors } from '@nhtio/lucid-resourceful'
class ResourcefulErrorHandler {
static handle(error: unknown, response: Response) {
// Configuration and setup errors
if (guards.isInstanceOf(error, 'E_INVALID_RESOURCEFUL_DATA_TYPE_OPTIONS', errors.E_INVALID_RESOURCEFUL_DATA_TYPE_OPTIONS)) {
return this.handleConfigurationError(error, response)
}
if (guards.isInstanceOf(error, 'E_INVALID_RESOURCEFUL_MIXIN_OPTIONS', errors.E_INVALID_RESOURCEFUL_MIXIN_OPTIONS)) {
return this.handleConfigurationError(error, response)
}
if (guards.isInstanceOf(error, 'E_INVALID_RESOURCEFUL_DECORATOR_OPTIONS', errors.E_INVALID_RESOURCEFUL_DECORATOR_OPTIONS)) {
return this.handleConfigurationError(error, response)
}
// Validation errors
if (guards.isInstanceOf(error, 'E_INVALID_PAYLOAD_EXCEPTION', errors.E_INVALID_PAYLOAD_EXCEPTION)) {
return this.handleValidationError(error, response)
}
if (guards.isInstanceOf(error, 'E_FORBIDDEN_PAYLOAD_EXCEPTION', errors.E_FORBIDDEN_PAYLOAD_EXCEPTION)) {
return this.handleForbiddenPayloadError(error, response)
}
if (guards.isInstanceOf(error, 'E_INVALID_RESOUREFUL_INDEX_REQUEST_EXCEPTION', errors.E_INVALID_RESOUREFUL_INDEX_REQUEST_EXCEPTION)) {
return this.handleIndexRequestError(error, response)
}
// Access control errors
if (guards.isInstanceOf(error, 'E_FORBIDDEN', errors.E_FORBIDDEN)) {
return this.handleForbiddenError(error, response)
}
if (guards.isInstanceOf(error, 'E_INVALID_COLUMN_ACCESS', errors.E_INVALID_COLUMN_ACCESS)) {
return this.handleColumnAccessError(error, response)
}
// Query and database errors
if (guards.isInstanceOf(error, 'E_RECORD_NOT_FOUND_EXCEPTION', errors.E_RECORD_NOT_FOUND_EXCEPTION)) {
return this.handleNotFoundError(error, response)
}
if (guards.isInstanceOf(error, 'E_MISSING_PRIMARY_KEY_EXCEPTION', errors.E_MISSING_PRIMARY_KEY_EXCEPTION)) {
return this.handleMissingPrimaryKeyError(error, response)
}
if (guards.isInstanceOf(error, 'E_LUCENE_SYNTAX_EXCEPTION', errors.E_LUCENE_SYNTAX_EXCEPTION)) {
return this.handleLuceneSyntaxError(error, response)
}
if (guards.isInstanceOf(error, 'E_INVALID_LUCENE_QUERY', errors.E_INVALID_LUCENE_QUERY)) {
return this.handleInvalidLuceneQuery(error, response)
}
if (guards.isInstanceOf(error, 'E_UNEXPECTED_COLUMN_IN_QUERY', errors.E_UNEXPECTED_COLUMN_IN_QUERY)) {
return this.handleUnexpectedColumnError(error, response)
}
// Unknown error
return this.handleUnknownError(error, response)
}
private static handleValidationError(error: errors.E_INVALID_PAYLOAD_EXCEPTION, response: Response) {
return response.status(422).json({
error: 'VALIDATION_ERROR',
message: 'The provided data is invalid',
details: error.details?.map(detail => ({
field: detail.path.join('.'),
message: detail.message,
value: detail.context?.value
}))
})
}
private static handleForbiddenError(error: errors.E_FORBIDDEN, response: Response) {
return response.status(403).json({
error: 'FORBIDDEN',
message: 'You do not have permission to perform this action'
})
}
private static handleNotFoundError(error: errors.E_RECORD_NOT_FOUND_EXCEPTION, response: Response) {
return response.status(404).json({
error: 'NOT_FOUND',
message: 'The requested resource was not found'
})
}
private static handleLuceneSyntaxError(error: errors.E_LUCENE_SYNTAX_EXCEPTION, response: Response) {
return response.status(422).json({
error: 'QUERY_SYNTAX_ERROR',
message: 'Invalid query syntax',
details: {
line: error.line,
column: error.column,
offset: error.offset
}
})
}
// ... other error handlers
}Error Context and Debugging
Joi Validation Details
Many Lucid Resourceful errors include Joi validation details that provide structured information about validation failures:
try {
const user = await User.$onResourcefulCreate(invalidPayload, ctx, app)
} catch (error) {
if (error instanceof E_INVALID_PAYLOAD_EXCEPTION && error.details) {
error.details.forEach(detail => {
console.log(`Field: ${detail.path.join('.')}`)
console.log(`Message: ${detail.message}`)
console.log(`Value: ${detail.context?.value}`)
console.log(`Label: ${detail.context?.label}`)
})
}
}Error Chaining
Lucid Resourceful errors preserve the original error cause when wrapping lower-level errors:
try {
// Operation that might fail
} catch (error) {
if (error instanceof E_LUCENE_SYNTAX_EXCEPTION) {
console.log('Resourceful error:', error.message)
console.log('Original error:', error.cause) // Original Lucene parser error
}
}Development vs Production Error Handling
Handle errors differently based on environment:
class ErrorHandler {
static handle(error: unknown, response: Response, isDevelopment: boolean = false) {
if (guards.isInstanceOf(error, 'E_INVALID_PAYLOAD_EXCEPTION', E_INVALID_PAYLOAD_EXCEPTION)) {
const responseData = {
error: 'VALIDATION_ERROR',
message: 'Invalid input data'
}
if (isDevelopment) {
// Include detailed validation information in development
responseData.details = error.details
responseData.stack = error.stack
}
return response.status(422).json(responseData)
}
// Handle other errors similarly
}
}Best Practices
1. Use Type Guards for Reliable Error Checking
Always use the isInstanceOf type guard for error checking to ensure compatibility across different bundling and transformation scenarios:
// ✅ Good: Using type guard
if (guards.isInstanceOf(error, 'E_FORBIDDEN', E_FORBIDDEN)) {
// Handle forbidden error
}
// ❌ Avoid: Direct instanceof (may not work with bundled code)
if (error instanceof E_FORBIDDEN) {
// This might fail in some environments
}2. Provide User-Friendly Error Messages
Transform technical error messages into user-friendly ones:
const ERROR_MESSAGES = {
E_INVALID_PAYLOAD_EXCEPTION: 'Please check your input and try again',
E_FORBIDDEN: 'You do not have permission to perform this action',
E_RECORD_NOT_FOUND_EXCEPTION: 'The requested item was not found',
E_LUCENE_SYNTAX_EXCEPTION: 'Your search query is invalid'
}
function getUserFriendlyMessage(error: unknown): string {
if (error instanceof Error && error.code) {
return ERROR_MESSAGES[error.code as keyof typeof ERROR_MESSAGES] || 'An unexpected error occurred'
}
return 'An unexpected error occurred'
}3. Log Errors for Debugging
Always log errors with sufficient context for debugging:
import { Logger } from '@adonisjs/logger'
class ErrorLogger {
static log(error: unknown, context: Record<string, any> = {}) {
if (guards.isInstanceOf(error, 'E_INVALID_PAYLOAD_EXCEPTION', E_INVALID_PAYLOAD_EXCEPTION)) {
Logger.warn('Validation error occurred', {
error: error.message,
details: error.details,
context
})
} else if (guards.isInstanceOf(error, 'E_FORBIDDEN', E_FORBIDDEN)) {
Logger.warn('Access denied', {
error: error.message,
context
})
} else {
Logger.error('Unexpected error', {
error: error.message,
stack: error.stack,
context
})
}
}
}4. Handle Validation Details Appropriately
Extract useful information from Joi validation details:
function formatValidationErrors(details: ValidationError['details'] = []) {
return details.map(detail => {
const field = detail.path.join('.')
const message = detail.message.replace(/["']/g, '') // Remove quotes
return {
field,
message,
value: detail.context?.value,
code: detail.type
}
})
}5. Graceful Degradation
Handle errors gracefully to maintain application stability:
async function safeOperation<T>(
operation: () => Promise<T>,
fallback: T
): Promise<T> {
try {
return await operation()
} catch (error) {
// Log the error
ErrorLogger.log(error)
// Return fallback value instead of crashing
return fallback
}
}
// Usage
const users = await safeOperation(
() => User.$onResourcefulIndex(query, page, perPage, fields, ctx, app),
{ records: [], total: 0, page: 1, perPage: 10 }
)API Reference
For detailed API documentation of error classes, see: