From 2a0775bd4a3b21e5b3d3b180d808e8f6cc8cec5c Mon Sep 17 00:00:00 2001 From: MKPLKN <15703708+MKPLKN@users.noreply.github.com> Date: Tue, 10 Mar 2026 00:03:52 +0200 Subject: [PATCH 1/2] add structured errors --- lib/codegen.js | 54 ++++++++++++++++++++++++++++++++++++++++++++------ test/basic.js | 2 +- 2 files changed, 49 insertions(+), 7 deletions(-) diff --git a/lib/codegen.js b/lib/codegen.js index fa3c2d4..36d5fbf 100644 --- a/lib/codegen.js +++ b/lib/codegen.js @@ -18,6 +18,44 @@ module.exports = function generateCode(hyperdispatch, { esm = false }) { str += 'const defaultVersion = version\n' str += '\n' + str += 'const ERRORS = {\n' + str += " NONEXISTENT_ROUTE: 'NONEXISTENT_ROUTE',\n" + str += " HANDLER_NOT_FOUND_BY_ID: 'HANDLER_NOT_FOUND_BY_ID',\n" + str += " ROUTE_NOT_FOUND_BY_NAME: 'ROUTE_NOT_FOUND_BY_NAME'\n" + str += '}\n' + str += '\n' + + str += 'class DispatchError extends Error {\n' + str += ' constructor (code, message, fn = DispatchError) {\n' + str += ' super(message ? `${code}: ${message}` : code)\n' + str += ' this.code = code\n' + str += ' this.isDispatchError = true\n' + str += ' if (Error.captureStackTrace) {\n' + str += ' Error.captureStackTrace(this, fn)\n' + str += ' }\n' + str += ' }\n' + str += '\n' + str += ' static isDispatchError (err) {\n' + str += ' return err?.isDispatchError === true\n' + str += ' }\n' + str += '\n' + str += ' static NONEXISTENT_ROUTE (name) {\n' + str += + ' return new DispatchError(ERRORS.NONEXISTENT_ROUTE, name, DispatchError.NONEXISTENT_ROUTE)\n' + str += ' }\n' + str += '\n' + str += ' static HANDLER_NOT_FOUND_BY_ID (id) {\n' + str += + ' return new DispatchError(ERRORS.HANDLER_NOT_FOUND_BY_ID, id, DispatchError.HANDLER_NOT_FOUND_BY_ID)\n' + str += ' }\n' + str += '\n' + str += ' static ROUTE_NOT_FOUND_BY_NAME (name) {\n' + str += + ' return new DispatchError(ERRORS.ROUTE_NOT_FOUND_BY_NAME, name, DispatchError.ROUTE_NOT_FOUND_BY_NAME)\n' + str += ' }\n' + str += '}\n' + str += '\n' + str += 'class Router {\n' str += ' constructor () {\n' for (const handler of hyperdispatch.handlers) { @@ -35,7 +73,7 @@ module.exports = function generateCode(hyperdispatch, { esm = false }) { str += ' break\n' } str += ' default:\n' - str += " throw new Error('Cannot register a handler for a nonexistent route: ' + name)\n" + str += ' throw DispatchError.NONEXISTENT_ROUTE(name)\n' str += ' }\n' str += ' this._missing--\n' str += ' }\n' @@ -61,7 +99,7 @@ module.exports = function generateCode(hyperdispatch, { esm = false }) { str += ` return this._handler${handler.id}(op.value, context)\n` } str += ' default:\n' - str += " throw new Error('Handler not found for ID:' + op.id)\n" + str += ' throw DispatchError.HANDLER_NOT_FOUND_BY_ID(op.id)\n' str += ' }\n' str += ' }\n' str += '}\n' @@ -114,7 +152,7 @@ module.exports = function generateCode(hyperdispatch, { esm = false }) { str += ` return route${handler.id}\n` } str += ' default:\n' - str += " throw new Error('Handler not found for name: ' + name)\n" + str += ' throw DispatchError.ROUTE_NOT_FOUND_BY_NAME(name)\n' str += ' }\n' str += '}\n' str += '\n' @@ -126,7 +164,7 @@ module.exports = function generateCode(hyperdispatch, { esm = false }) { str += ` return route${handler.id}\n` } str += ' default:\n' - str += " throw new Error('Handler not found for ID: ' + id)\n" + str += ' throw DispatchError.HANDLER_NOT_FOUND_BY_ID(id)\n' str += ' }\n' str += '}\n' str += '\n' @@ -136,14 +174,18 @@ module.exports = function generateCode(hyperdispatch, { esm = false }) { str += ' version,\n' str += ' encode,\n' str += ' decode,\n' - str += ' Router\n' + str += ' Router,\n' + str += ' DispatchError,\n' + str += ' ERRORS\n' str += '}\n' } else { str += 'module.exports = {\n' str += ' version,\n' str += ' encode,\n' str += ' decode,\n' - str += ' Router\n' + str += ' Router,\n' + str += ' DispatchError,\n' + str += ' ERRORS\n' str += '}\n' } diff --git a/test/basic.js b/test/basic.js index 19a5f9d..12523cb 100644 --- a/test/basic.js +++ b/test/basic.js @@ -393,7 +393,7 @@ test('basic dispatch to non-existent route throws', async (t) => { await r.dispatch(encode('@test/test-request-1', { id: 10, str: 'hello' }), 'some-context') const badMsg = { id: -1, name: '@test/invalid', value: 'error' } - await t.exception(r.dispatch(badMsg, 'invalid-context'), /Handler not found for ID:-1/) + await t.exception(r.dispatch(badMsg, 'invalid-context'), /HANDLER_NOT_FOUND_BY_ID/) }) test('can both encode and decode string enums', async (t) => { From 7bbe591a897cefc0f30c7a787730802ad8f1ee99 Mon Sep 17 00:00:00 2001 From: MKPLKN <15703708+MKPLKN@users.noreply.github.com> Date: Tue, 10 Mar 2026 00:37:01 +0200 Subject: [PATCH 2/2] import errors from runtime --- lib/codegen.js | 42 ++---------------------------------------- lib/errors.js | 46 ++++++++++++++++++++++++++++++++++++++++++++++ runtime.cjs | 6 +++++- runtime.mjs | 3 ++- 4 files changed, 55 insertions(+), 42 deletions(-) create mode 100644 lib/errors.js diff --git a/lib/codegen.js b/lib/codegen.js index 36d5fbf..a305864 100644 --- a/lib/codegen.js +++ b/lib/codegen.js @@ -7,10 +7,10 @@ module.exports = function generateCode(hyperdispatch, { esm = false }) { str += '\n' if (esm) { - str += "import { c, b4a, assert } from 'hyperdispatch/runtime'\n" + str += "import { c, b4a, assert, DispatchError, ERRORS } from 'hyperdispatch/runtime'\n" str += "import { version, getEncoding, setVersion } from './messages.js'\n" } else { - str += "const { c, b4a, assert } = require('hyperdispatch/runtime')\n" + str += "const { c, b4a, assert, DispatchError, ERRORS } = require('hyperdispatch/runtime')\n" str += "const { version, getEncoding, setVersion } = require('./messages.js')\n" } @@ -18,44 +18,6 @@ module.exports = function generateCode(hyperdispatch, { esm = false }) { str += 'const defaultVersion = version\n' str += '\n' - str += 'const ERRORS = {\n' - str += " NONEXISTENT_ROUTE: 'NONEXISTENT_ROUTE',\n" - str += " HANDLER_NOT_FOUND_BY_ID: 'HANDLER_NOT_FOUND_BY_ID',\n" - str += " ROUTE_NOT_FOUND_BY_NAME: 'ROUTE_NOT_FOUND_BY_NAME'\n" - str += '}\n' - str += '\n' - - str += 'class DispatchError extends Error {\n' - str += ' constructor (code, message, fn = DispatchError) {\n' - str += ' super(message ? `${code}: ${message}` : code)\n' - str += ' this.code = code\n' - str += ' this.isDispatchError = true\n' - str += ' if (Error.captureStackTrace) {\n' - str += ' Error.captureStackTrace(this, fn)\n' - str += ' }\n' - str += ' }\n' - str += '\n' - str += ' static isDispatchError (err) {\n' - str += ' return err?.isDispatchError === true\n' - str += ' }\n' - str += '\n' - str += ' static NONEXISTENT_ROUTE (name) {\n' - str += - ' return new DispatchError(ERRORS.NONEXISTENT_ROUTE, name, DispatchError.NONEXISTENT_ROUTE)\n' - str += ' }\n' - str += '\n' - str += ' static HANDLER_NOT_FOUND_BY_ID (id) {\n' - str += - ' return new DispatchError(ERRORS.HANDLER_NOT_FOUND_BY_ID, id, DispatchError.HANDLER_NOT_FOUND_BY_ID)\n' - str += ' }\n' - str += '\n' - str += ' static ROUTE_NOT_FOUND_BY_NAME (name) {\n' - str += - ' return new DispatchError(ERRORS.ROUTE_NOT_FOUND_BY_NAME, name, DispatchError.ROUTE_NOT_FOUND_BY_NAME)\n' - str += ' }\n' - str += '}\n' - str += '\n' - str += 'class Router {\n' str += ' constructor () {\n' for (const handler of hyperdispatch.handlers) { diff --git a/lib/errors.js b/lib/errors.js new file mode 100644 index 0000000..8f37e5d --- /dev/null +++ b/lib/errors.js @@ -0,0 +1,46 @@ +const ERRORS = { + NONEXISTENT_ROUTE: 'NONEXISTENT_ROUTE', + HANDLER_NOT_FOUND_BY_ID: 'HANDLER_NOT_FOUND_BY_ID', + ROUTE_NOT_FOUND_BY_NAME: 'ROUTE_NOT_FOUND_BY_NAME' +} + +class DispatchError extends Error { + constructor(code, message, fn = DispatchError) { + super(message ? `${code}: ${message}` : code) + this.code = code + this.isDispatchError = true + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, fn) + } + } + + static isDispatchError(err) { + return err?.isDispatchError === true + } + + static NONEXISTENT_ROUTE(name) { + return new DispatchError(ERRORS.NONEXISTENT_ROUTE, name, DispatchError.NONEXISTENT_ROUTE) + } + + static HANDLER_NOT_FOUND_BY_ID(id) { + return new DispatchError( + ERRORS.HANDLER_NOT_FOUND_BY_ID, + id, + DispatchError.HANDLER_NOT_FOUND_BY_ID + ) + } + + static ROUTE_NOT_FOUND_BY_NAME(name) { + return new DispatchError( + ERRORS.ROUTE_NOT_FOUND_BY_NAME, + name, + DispatchError.ROUTE_NOT_FOUND_BY_NAME + ) + } +} + +module.exports = { + DispatchError, + ERRORS +} diff --git a/runtime.cjs b/runtime.cjs index fe74b6c..1e02822 100644 --- a/runtime.cjs +++ b/runtime.cjs @@ -1,5 +1,9 @@ +const { DispatchError, ERRORS } = require('./lib/errors') + module.exports = { c: require('compact-encoding'), b4a: require('b4a'), - assert: require('nanoassert') + assert: require('nanoassert'), + DispatchError, + ERRORS } diff --git a/runtime.mjs b/runtime.mjs index a6d9c11..81824b8 100644 --- a/runtime.mjs +++ b/runtime.mjs @@ -1,5 +1,6 @@ import c from 'compact-encoding' import b4a from 'b4a' import assert from 'nanoassert' +import { DispatchError, ERRORS } from './lib/errors' -export { c, b4a, assert } +export { c, b4a, assert, DispatchError, ERRORS }