@ololoepepe/errors
v0.2.1
Published
Common errors library
Readme
@ololoepepe/errors
Common errors library — a small set of Error subclasses for HTTP-style APIs, written in TypeScript.
Every error carries an HTTP status code and an optional, arbitrarily shaped body alongside the usual
Error fields, so an error thrown deep in the application already knows how it should be rendered by
the transport layer.
Requirements
- Node.js >= 24
- ESM only (the package is
"type": "module")
Installation
npm install @ololoepepe/errorsUsage
import {ApiError, NotFoundError, ValidationError} from '@ololoepepe/errors';
// A plain message with the default 500 status.
throw new ApiError('Something went wrong');
// An explicit status code and body.
throw new ApiError('Payment Required', 402, {invoiceId: 42});
// Wrapping an existing error keeps its stack (see `originalStack` below).
try {
await doSomething();
} catch (error) {
throw new ApiError(error, 502);
}Rendering an error in, say, an Express error handler:
app.use((error, req, res, next) => {
if (!(error instanceof ApiError)) {
next(error);
return;
}
res.status(error.statusCode).json({
body: error.body,
message: error.message
});
});TypeScript
ApiError is generic over its body type. The type parameter is inferred from the body argument and
defaults to unknown:
import {ApiError, NotFoundError} from '@ololoepepe/errors';
const error = new ApiError('Payment Required', 402, {invoiceId: 42});
const invoiceId: number | undefined = error.body?.invoiceId;
// Subclasses declare their own body type.
const notFound = new NotFoundError('user');
const entity: string | undefined = notFound.body?.entity;
// It can also be pinned explicitly.
const explicit = new ApiError<{reason: string}>('Conflict', 409);API
class ApiError<TBody = unknown> extends Error
new ApiError(error, status?, body?)
| Parameter | Type | Default | Description |
|-----------|-------------------|---------|--------------------------------------------------------|
| error | Error \| string | — | The original error, or the message to use. |
| status | number | 500 | The HTTP status code. |
| body | TBody | — | An arbitrarily shaped payload describing the error. |
When error is an Error instance, its message and stack are adopted by the new error: stack
is replaced with the original one (as a non-writable property) and the stack captured at the
ApiError construction site is kept in originalStack. When error is a string, it is used as the
message and originalStack stays null.
Properties
All properties are read-only getters.
| Property | Type | Description |
|-----------------|-------------------|---------------------------------------------------------------------------------|
| body | TBody \| null | The body passed to the constructor, or null if none was passed. |
| message | string | The error message. |
| name | string | The name of the error class. Subclasses report their own name automatically. |
| originalStack | string \| null | The stack captured at the construction site when wrapping an existing error. |
| statusCode | number | The HTTP status code. |
name is derived from the constructor, so custom subclasses need no extra wiring:
class TeapotError extends ApiError {}
new TeapotError('I am a teapot', 418).name; // 'TeapotError'class NotFoundError extends ApiError<NotFoundErrorBody>
new NotFoundError(entity)
Passes the message 'Not Found', the status code 404 and the body {entity} to ApiError.
| Parameter | Type | Description |
|-----------|----------|-----------------------------------|
| entity | string | The name of the missing entity. |
interface NotFoundErrorBody {
entity: string;
}class ValidationError extends ApiError<ValidationErrorBody>
new ValidationError(originalError)
Passes the message 'Bad Request', the status code 400 and the body {originalError} to ApiError.
| Parameter | Type | Description |
|-----------------|-----------|-----------------------------------|
| originalError | unknown | The underlying validation error. |
interface ValidationErrorBody {
originalError: unknown;
}Development
npm run lint # ESLint
npm run typecheck # tsc --noEmit
npm test # node:test
npm run build # emit dist/node/The sources are run directly by Node via type stripping, so they must stay erasable — no enums,
namespaces or parameter properties (erasableSyntaxOnly is enabled).
Every internal import goes through the #src/*.ts subpath map declared in package.json — there are
no relative imports, in src or in test. Neither tsc nor any other tool rewrites those
specifiers, so npm run build drops a package.json of its own into dist/node/ that remaps
#src/*.ts to the sibling .js file. That is what lets the published package resolve without ever
looking at src.
License
UNLICENSED
