Error Proxy System
Problem
Referencing typed errors inside handlers required verbose string-keyed
destructuring that clashed with normal JavaScript patterns. The dot-notation
key format — necessary to encode the realm.errorName ownership — forced
developers to use the explicit rename syntax:
export default handler(
({errors: {'release.jobTrigger': errorReleaseJobTrigger}}) =>
async function releaseJobTrigger(params, $meta) {
throw errorReleaseJobTrigger({params: {jobName: 'test'}}, $meta);
}
);
This had three compounding issues:
- IDE friction — string-keyed destructuring provides no autocomplete and the rename requirement is unfamiliar to most JavaScript developers.
- Typo risk deferred to runtime — a misspelled key in a string literal
(
'release.jobTriger') would not be caught until the error was actually thrown, potentially in production. - Verbosity — every error reference required two tokens (the string key and the local variable name) even when both encode the same information.
Solution
A JavaScript Proxy wraps the errors map at handler registration time, enabling
a simplified camelCase destructuring syntax while preserving full backwards
compatibility. The proxy converts property access from
errorReleaseJobTrigger → release.jobTrigger via a case-insensitive lookup,
performing the transformation at definition time (once, not per call).
export default handler(
({errors: {errorReleaseJobTrigger}}) =>
async function releaseJobTrigger(params, $meta) {
throw errorReleaseJobTrigger({params: {jobName: 'test'}}, $meta);
}
);
Overview
The Blong framework supports a simplified syntax for referencing errors in handlers, making error handling more ergonomic while maintaining full backwards compatibility.
What Changed
Before (Legacy Syntax - Still Supported)
export default handler(
({errors: {'release.jobTrigger': errorReleaseJobTrigger}}) =>
async function releaseJobTrigger(params, $meta) {
throw errorReleaseJobTrigger({params: {jobName: 'test'}}, $meta);
}
);
After (New Simplified Syntax - Recommended)
export default handler(
({errors: {errorReleaseJobTrigger}}) =>
async function releaseJobTrigger(params, $meta) {
throw errorReleaseJobTrigger({params: {jobName: 'test'}}, $meta);
}
);
Key Benefits
- Simpler Syntax: No more string quotes and explicit renaming
- Better IDE Support: Improved autocomplete with direct property access
- Case-Insensitive:
errorReleaseJobTrigger,errorreleasjobtrigger, andERRORRELEASEJOBTRIGGERall work - Backwards Compatible: Old dot notation syntax continues to work
- Less Typing: Cleaner, more concise code
- Early Error Detection: Typos in error names throw immediately during destructuring, catching bugs early
How It Works
The error system now uses a JavaScript Proxy that:
- Maintains Original Keys: Errors are still stored with their dot notation (e.g.,
'release.jobTrigger') - Provides Multiple Access Patterns:
- Direct dot notation:
errors['release.jobTrigger'](backwards compatible) - Simplified camelCase:
errors.errorReleaseJobTrigger(new) - Destructuring:
{errorReleaseJobTrigger}fromerrors(new)
- Direct dot notation:
- Case-Insensitive Lookup: Converts property names to lowercase for matching
- Optional Error Prefix: Works with or without the "error" prefix
- Immediate Error on Typos: Throws immediately when accessing non-existent errors to catch typos during destructuring
Naming Convention
Error keys are automatically mapped to camelCase variables:
| Error Key (Definition) | Variable Name (Usage) |
|---|---|
'release.jobTrigger' | errorReleaseJobTrigger |
'user.notFound' | errorUserNotFound |
'payment.insufficientFunds' | errorPaymentInsufficientFunds |
'hsm.connection.timeout' | errorHsmConnectionTimeout |
Implementation Details
Internal Storage
- Errors are stored with their original dot-notation keys
- A secondary lookup map stores lowercase, no-dot versions for fast matching
- Example:
'release.jobTrigger'→ lookup key:'releasejobtrigger'
Proxy Logic
The error proxy is created once upfront and cached for performance. When accessing errors.errorReleaseJobTrigger:
- Check if property exists directly (for backwards compatibility)
- If not, convert to lowercase:
'errorReleaseJobTrigger'→'errorreleasjobtrigger' - Remove "error" prefix if present:
'errorreleasjobtrigger'→'releasejobtrigger' - Look up in the error lookup map:
'releasejobtrigger'→'release.jobTrigger' - Return the error handler for
'release.jobTrigger'
Performance: The proxy is instantiated once and reused on all subsequent get() calls, avoiding repeated proxy creation overhead.
Real Implementation
The proxy implementation lives in core/blong-gogo/src/error.ts:
const errorsProxy = new Proxy(errors, {
get(target, prop: string | symbol) {
if (typeof prop === 'symbol') return target[prop];
if (prop in target) return target[prop]; // direct/dot-notation access
let lookupKey = (prop as string).toLowerCase();
if (lookupKey.startsWith('error')) {
lookupKey = lookupKey.substring(5); // strip 'error' prefix
}
const originalKey = errorLookup[lookupKey];
if (originalKey && target[originalKey]) return target[originalKey];
// Throws immediately — catches typos at destructure time
throw new Error(
`Error '${String(prop)}' not found. Available errors: ${Object.keys(target).join(', ')}`,
);
},
has(target, prop) { /* ... */ }
});
Type Safety
The proxy maintains all error handler properties:
type: Original error key (e.g.,'release.jobTrigger')message: Error message templateparams: Array of parameter names extracted from message templateprint: Optional print message
Error Detection
Typo Protection: The proxy throws immediately when accessing a non-existent error:
const errors = errorFactory.get();
// This throws immediately with a helpful message
const {errorTypoInName} = errors;
// Error: Error 'errorTypoInName' not found. Available errors: release.jobTrigger, release.ping, ...
// Catches typos during destructuring
const {errorReleaseJobTrigger, errorWrongName} = errors;
// Error: Error 'errorWrongName' not found. Available errors: ...
This behaviour ensures that typos and incorrect error names are caught immediately during development, rather than failing silently or at runtime.
Migration Guide
No Breaking Changes
All existing code continues to work. Migration is optional but recommended for new code.
Recommended Migration Path
- New Handlers: Use simplified syntax from the start
- Existing Handlers: Update incrementally as you touch the code
- No Rush: Both syntaxes can coexist in the same codebase
Example Migration
// Before
export default handler(
({
errors: {
'user.notFound': errorUserNotFound,
'user.invalidEmail': errorUserInvalidEmail,
'user.exists': errorUserExists
}
}) => {
// handler implementation
}
);
// After
export default handler(
({
errors: {
errorUserNotFound,
errorUserInvalidEmail,
errorUserExists
}
}) => {
// handler implementation
}
);
Testing
Comprehensive test coverage in core/blong-gogo/src/error.proxy.test.ts:
- ✅ Backwards compatibility with dot notation
- ✅ New camelCase access patterns
- ✅ Destructuring with both syntaxes
- ✅ Case-insensitive matching
- ✅ Parameterized errors
- ✅ Multi-part error names
- ✅ Immediate error throwing for non-existent errors
- ✅ Destructuring typo detection
- ✅ Property preservation
- ✅ Proxy instance caching (singleton pattern)
Run with:
cd core/blong-gogo
node --test src/error.proxy.test.ts
Future Ideas
-
Type-safe proxy via TypeScript template literals — derive the camelCase property type from the dot-notation key at compile time using
type ErrorKey<T extends string> =`error${Capitalize<CamelCase<T>>}`so that
errors.errorReleaseJobTriggeris type-checked against the defined error registry without runtime cost. -
Namespace sub-setting — allow
errors.subset('release')to return a proxy that only exposesrelease.*errors. This reduces the surface area visible in a handler and makes it easier to understand which errors a given handler can throw. -
Auto-generated error documentation — expose an
errors.all()method returning the full list of error types, messages, and HTTP status codes. The framework can call this at startup to populate the OpenAPIresponsessection automatically, ensuring API documentation stays in sync with the error registry. -
Coverage for error handling in tests — extend the test framework to track which errors are thrown during test execution and report on error coverage, similar to code coverage. This encourages testing of edge cases and ensures that all defined errors are exercised by tests.