|
| 1 | +--- |
| 2 | +title: "Error Handling" |
| 3 | +description: "Structured error handling with ObjectQLError — error codes, patterns, and best practices" |
| 4 | +--- |
| 5 | + |
| 6 | +ObjectQL uses a structured error handling pattern built around the `ObjectQLError` class. All packages in the ecosystem throw `ObjectQLError` instead of plain `Error` objects, providing consistent error codes, messages, and optional details across every layer. |
| 7 | + |
| 8 | +--- |
| 9 | + |
| 10 | +## ObjectQLError Class |
| 11 | + |
| 12 | +```typescript |
| 13 | +import { ObjectQLError, ApiErrorCode } from '@objectql/types'; |
| 14 | + |
| 15 | +throw new ObjectQLError({ |
| 16 | + code: ApiErrorCode.NOT_FOUND, |
| 17 | + message: 'Object "orders" not found in metadata registry', |
| 18 | + details: { field: 'objectName', reason: 'Object not registered' } |
| 19 | +}); |
| 20 | +``` |
| 21 | + |
| 22 | +### Constructor |
| 23 | + |
| 24 | +| Parameter | Type | Required | Description | |
| 25 | +|-----------|------|----------|-------------| |
| 26 | +| `code` | `ApiErrorCode \| string` | ✅ | Semantic error code (see taxonomy below) | |
| 27 | +| `message` | `string` | ✅ | Human-readable error description | |
| 28 | +| `details` | `ApiErrorDetails` | ❌ | Structured metadata (field, reason, etc.) | |
| 29 | + |
| 30 | +### Properties |
| 31 | + |
| 32 | +- **`code`** — The error code string (e.g., `'NOT_FOUND'`, `'DRIVER_QUERY_FAILED'`) |
| 33 | +- **`message`** — Human-readable error message (inherited from `Error`) |
| 34 | +- **`details`** — Optional structured metadata for programmatic error inspection |
| 35 | +- **`name`** — Always `'ObjectQLError'` (useful for `instanceof` checks) |
| 36 | +- **`stack`** — Stack trace (inherited from `Error`) |
| 37 | + |
| 38 | +--- |
| 39 | + |
| 40 | +## Error Code Taxonomy |
| 41 | + |
| 42 | +### Core Error Codes (`ApiErrorCode` enum) |
| 43 | + |
| 44 | +| Code | When to Use | |
| 45 | +|------|------------| |
| 46 | +| `INVALID_REQUEST` | Malformed request parameters or missing required fields | |
| 47 | +| `VALIDATION_ERROR` | Field validation failure (type mismatch, constraint violation) | |
| 48 | +| `UNAUTHORIZED` | Missing or invalid authentication credentials | |
| 49 | +| `FORBIDDEN` | Authenticated but insufficient permissions (RBAC violation) | |
| 50 | +| `NOT_FOUND` | Object, record, or resource does not exist | |
| 51 | +| `CONFLICT` | Duplicate key, optimistic concurrency conflict | |
| 52 | +| `INTERNAL_ERROR` | Unexpected internal error (catch-all) | |
| 53 | +| `DATABASE_ERROR` | Generic database-level error | |
| 54 | +| `RATE_LIMIT_EXCEEDED` | Too many requests | |
| 55 | + |
| 56 | +### Driver Error Codes |
| 57 | + |
| 58 | +| Code | When to Use | |
| 59 | +|------|------------| |
| 60 | +| `DRIVER_ERROR` | Generic driver error | |
| 61 | +| `DRIVER_CONNECTION_FAILED` | Failed to connect to the database | |
| 62 | +| `DRIVER_QUERY_FAILED` | Query execution failed (validation, constraint, etc.) | |
| 63 | +| `DRIVER_TRANSACTION_FAILED` | Transaction begin/commit/rollback failed | |
| 64 | +| `DRIVER_UNSUPPORTED_OPERATION` | Operation not supported by this driver | |
| 65 | + |
| 66 | +### Protocol Error Codes |
| 67 | + |
| 68 | +| Code | When to Use | |
| 69 | +|------|------------| |
| 70 | +| `PROTOCOL_ERROR` | Generic protocol error | |
| 71 | +| `PROTOCOL_INVALID_REQUEST` | Invalid protocol request format | |
| 72 | +| `PROTOCOL_METHOD_NOT_FOUND` | Unknown method/endpoint in the protocol | |
| 73 | +| `PROTOCOL_BATCH_ERROR` | Batch operation failure | |
| 74 | + |
| 75 | +### Plugin Error Codes |
| 76 | + |
| 77 | +| Code | When to Use | |
| 78 | +|------|------------| |
| 79 | +| `TENANT_ISOLATION_VIOLATION` | Cross-tenant data access attempt | |
| 80 | +| `TENANT_NOT_FOUND` | Tenant context missing or invalid | |
| 81 | +| `WORKFLOW_TRANSITION_DENIED` | Invalid state machine transition | |
| 82 | +| `FORMULA_EVALUATION_ERROR` | Formula expression evaluation failed | |
| 83 | + |
| 84 | +### Tool/CLI Error Codes |
| 85 | + |
| 86 | +| Code | When to Use | |
| 87 | +|------|------------| |
| 88 | +| `CONFIG_ERROR` | Configuration file missing or invalid | |
| 89 | +| `SCAFFOLD_ERROR` | Project scaffolding failure | |
| 90 | +| `MIGRATION_ERROR` | Migration execution failure | |
| 91 | + |
| 92 | +--- |
| 93 | + |
| 94 | +## Error Handling Patterns |
| 95 | + |
| 96 | +### Catching ObjectQLError |
| 97 | + |
| 98 | +```typescript |
| 99 | +import { ObjectQLError } from '@objectql/types'; |
| 100 | + |
| 101 | +try { |
| 102 | + const record = await repo.findOne(id); |
| 103 | +} catch (error) { |
| 104 | + if (error instanceof ObjectQLError) { |
| 105 | + // Structured error — inspect code and details |
| 106 | + switch (error.code) { |
| 107 | + case 'NOT_FOUND': |
| 108 | + return { status: 404, message: error.message }; |
| 109 | + case 'VALIDATION_ERROR': |
| 110 | + return { status: 400, fields: error.details?.fields }; |
| 111 | + case 'FORBIDDEN': |
| 112 | + return { status: 403, permission: error.details?.required_permission }; |
| 113 | + default: |
| 114 | + return { status: 500, message: 'Internal server error' }; |
| 115 | + } |
| 116 | + } |
| 117 | + // Unknown error — re-throw |
| 118 | + throw error; |
| 119 | +} |
| 120 | +``` |
| 121 | + |
| 122 | +### Throwing in Drivers |
| 123 | + |
| 124 | +```typescript |
| 125 | +import { ObjectQLError } from '@objectql/types'; |
| 126 | + |
| 127 | +async find(objectName: string, query: UnifiedQuery) { |
| 128 | + try { |
| 129 | + return await this.db.collection(objectName).find(query.filter).toArray(); |
| 130 | + } catch (error: any) { |
| 131 | + throw new ObjectQLError({ |
| 132 | + code: 'DRIVER_QUERY_FAILED', |
| 133 | + message: `MongoDB query failed on "${objectName}": ${error.message}`, |
| 134 | + details: { field: 'query', reason: error.code } |
| 135 | + }); |
| 136 | + } |
| 137 | +} |
| 138 | +``` |
| 139 | + |
| 140 | +### Throwing in Hooks |
| 141 | + |
| 142 | +```typescript |
| 143 | +import { ObjectQLError, ApiErrorCode } from '@objectql/types'; |
| 144 | + |
| 145 | +export async function beforeInsert(context: HookContext) { |
| 146 | + const { doc } = context; |
| 147 | + |
| 148 | + if (!doc.email?.includes('@')) { |
| 149 | + throw new ObjectQLError({ |
| 150 | + code: ApiErrorCode.VALIDATION_ERROR, |
| 151 | + message: 'Invalid email address', |
| 152 | + details: { field: 'email', reason: 'Must contain @ symbol' } |
| 153 | + }); |
| 154 | + } |
| 155 | +} |
| 156 | +``` |
| 157 | + |
| 158 | +--- |
| 159 | + |
| 160 | +## ApiErrorDetails Reference |
| 161 | + |
| 162 | +The `details` object supports these common fields: |
| 163 | + |
| 164 | +| Field | Type | Description | |
| 165 | +|-------|------|-------------| |
| 166 | +| `field` | `string` | The field that caused the error | |
| 167 | +| `reason` | `string` | Human-readable reason for the error | |
| 168 | +| `fields` | `Record<string, string>` | Multiple field errors (for bulk validation) | |
| 169 | +| `required_permission` | `string` | The permission needed (for FORBIDDEN errors) | |
| 170 | +| `user_roles` | `string[]` | Current user's roles (for FORBIDDEN errors) | |
| 171 | +| `retry_after` | `number` | Seconds to wait before retrying (for RATE_LIMIT_EXCEEDED) | |
| 172 | + |
| 173 | +Additional custom fields can be added via the index signature `[key: string]: unknown`. |
| 174 | + |
| 175 | +--- |
| 176 | + |
| 177 | +## Best Practices |
| 178 | + |
| 179 | +1. **Always use `ObjectQLError`** — Never `throw new Error()` in packages |
| 180 | +2. **Choose specific error codes** — Use `DRIVER_QUERY_FAILED` over generic `INTERNAL_ERROR` |
| 181 | +3. **Include context in messages** — Include the object name, field name, or operation |
| 182 | +4. **Use `details` for machine-readable info** — Error messages are for humans, details are for code |
| 183 | +5. **Preserve original error info** — Include `error.message` in your ObjectQLError message |
| 184 | +6. **Check with `instanceof`** — Use `error instanceof ObjectQLError` for type-safe handling |
0 commit comments