Skip to content

Commit aae5dbd

Browse files
feat(error-handler): Add error handler package (#1004)
1 parent b3bbbfc commit aae5dbd

13 files changed

Lines changed: 532 additions & 63 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.eslintrc.cjs
2+
coverage
3+
dist
4+
node_modules
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
module.exports = {
2+
root: true,
3+
extends: ["@prefabs.tech/eslint-config/fastify"],
4+
};

packages/error-handler/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
**/*.log*
2+
/coverage
3+
/dist
4+
/node_modules

packages/error-handler/README.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# @prefabs.tech/fastify-error-handler
2+
3+
A [Fastify](https://github.com/fastify/fastify) plugin that provides an easy integration of error handler in fastify API.
4+
5+
## Requirements
6+
7+
* [@prefabs.tech/fastify-config](../config/)
8+
* [@fastify/sensible](https://github.com/fastify/fastify-sensible)
9+
10+
## Installation
11+
12+
Install with npm:
13+
14+
```bash
15+
npm install @prefabs.tech/fastify-error-handler
16+
```
17+
18+
Install with pnpm:
19+
20+
```bash
21+
pnpm add --filter "@scope/project @prefabs.tech/fastify-error-handler
22+
```
23+
24+
## Usage
25+
26+
### Register Plugin
27+
28+
Register @prefabs.tech/fastify-error-handler package with your Fastify instance:
29+
30+
Note: Register the errorHandler plugin as early as possible (Before all your routes and plugin registration).
31+
32+
```typescript
33+
import errorHandlerPlugin from "@prefabs.tech/fastify-error-handler";
34+
import Fastify from "fastify";
35+
36+
const start = async () => {
37+
// Create fastify instance
38+
const fastify = Fastify();
39+
40+
// Register fastify-error-handler plugin
41+
await fastify.register(errorHandlerPlugin, {});
42+
43+
await fastify.listen({
44+
port: config.port,
45+
host: "0.0.0.0",
46+
});
47+
};
48+
49+
start();
50+
```
51+
### Options
52+
53+
#### stackTrace
54+
55+
When enabled, the error handler will include the error’s stack trace in the HTTP response body.
56+
57+
By default, it is set to false.
58+
59+
```ts
60+
stackTrace?: boolean; // Default: false
61+
```
62+
63+
#### preErrorHandler
64+
65+
preErrorHandler is an optional error handler that runs before the default error handler logic.
66+
It allows you to intercept specific errors, handle them yourself, and prevent the default handler from running.
67+
68+
This is especially useful when you need to integrate with other libraries that have their own error formats — for example, handling SuperTokens errors before your API’s standard error response.
69+
70+
```ts
71+
preErrorHandler?: (
72+
error: FastifyError,
73+
request: FastifyRequest,
74+
reply: FastifyReply,
75+
) => void | Promise<void>;
76+
```
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
{
2+
"name": "@prefabs.tech/fastify-error-handler",
3+
"version": "0.88.2",
4+
"description": "Fastify error-handler plugin",
5+
"homepage": "https://github.com/prefabs-tech/fastify/tree/main/packages/error-handler#readme",
6+
"repository": {
7+
"type": "git",
8+
"url": "git+https://github.com/prefabs-tech/fastify.git",
9+
"directory": "packages/error-handler"
10+
},
11+
"license": "MIT",
12+
"type": "module",
13+
"exports": {
14+
".": {
15+
"import": "./dist/prefabs-tech-fastify-error-handler.js",
16+
"require": "./dist/prefabs-tech-fastify-error-handler.umd.cjs"
17+
}
18+
},
19+
"main": "./dist/prefabs-tech-fastify-error-handler.umd.cjs",
20+
"module": "./dist/prefabs-tech-fastify-error-handler.js",
21+
"types": "./dist/types/index.d.ts",
22+
"files": [
23+
"dist"
24+
],
25+
"scripts": {
26+
"build": "vite build && tsc --emitDeclarationOnly && mv dist/src dist/types",
27+
"lint": "eslint .",
28+
"lint:fix": "eslint . --fix",
29+
"sort-package": "npx sort-package-json",
30+
"typecheck": "tsc --noEmit -p tsconfig.json --composite false"
31+
},
32+
"dependencies": {
33+
"@fastify/sensible": "6.0.3",
34+
"stack-trace":"1.0.0-pre2"
35+
},
36+
"devDependencies": {
37+
"@prefabs.tech/eslint-config": "0.2.0",
38+
"@prefabs.tech/tsconfig": "0.2.0",
39+
"@types/node": "20.19.9",
40+
"@types/stack-trace": "0.0.33",
41+
"@typescript-eslint/eslint-plugin": "8.38.0",
42+
"@typescript-eslint/parser": "8.38.0",
43+
"@vitest/coverage-istanbul": "3.2.4",
44+
"eslint": "8.57.1",
45+
"eslint-config-prettier": "9.1.2",
46+
"eslint-import-resolver-alias": "1.1.2",
47+
"eslint-import-resolver-typescript": "3.10.1",
48+
"eslint-plugin-import": "2.32.0",
49+
"eslint-plugin-n": "14.0.0",
50+
"eslint-plugin-prettier": "5.5.3",
51+
"eslint-plugin-promise": "7.2.1",
52+
"eslint-plugin-unicorn": "56.0.1",
53+
"fastify": "5.4.0",
54+
"fastify-plugin": "5.0.1",
55+
"prettier": "3.6.2",
56+
"typescript": "5.8.3",
57+
"vite": "6.3.5",
58+
"vitest": "3.2.4"
59+
},
60+
"peerDependencies": {
61+
"fastify": ">=5.2.1",
62+
"fastify-plugin": ">=5.0.1"
63+
},
64+
"engines": {
65+
"node": ">=20"
66+
}
67+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { STATUS_CODES } from "node:http";
2+
3+
import { HttpError } from "@fastify/sensible";
4+
import { FastifyReply, FastifyRequest } from "fastify";
5+
import { parse } from "stack-trace";
6+
7+
import { CustomError } from "./utils/error";
8+
9+
import type { ErrorResponse } from "./types";
10+
11+
const getHttpStatusText = (statusCode: number): string =>
12+
STATUS_CODES[statusCode] ?? "Internal Server Error";
13+
14+
export const errorHandler = (
15+
error: Error,
16+
request: FastifyRequest,
17+
reply: FastifyReply,
18+
) => {
19+
const { log: logger } = request;
20+
21+
const isStackTraceEnabled = request.server.stackTrace || false;
22+
23+
const isHttpError = error instanceof HttpError;
24+
25+
if (isHttpError) {
26+
const statusCode = error.statusCode || 500;
27+
28+
if (statusCode >= 500) {
29+
logger.error(error);
30+
} else if (statusCode >= 400) {
31+
logger.info(error);
32+
} else {
33+
logger.error(error);
34+
}
35+
36+
const response: ErrorResponse = {
37+
code: error.code,
38+
error: error.error || getHttpStatusText(statusCode),
39+
message: error.message,
40+
name: error.name,
41+
statusCode,
42+
};
43+
44+
if (isStackTraceEnabled && error.stack) {
45+
response.stack = parse(error);
46+
}
47+
48+
void reply.code(statusCode).send(response);
49+
50+
return;
51+
}
52+
53+
let message = "Server error, please contact support";
54+
let code = "INTERNAL_SERVER_ERROR";
55+
56+
if (error instanceof CustomError) {
57+
code = error.code || code;
58+
message = "Server has an error that is not handled, please contact support";
59+
}
60+
61+
if (isStackTraceEnabled && error.stack) {
62+
const response: ErrorResponse = {
63+
code: code,
64+
message: error.message,
65+
name: error.name,
66+
statusCode: 500,
67+
};
68+
69+
response.stack = parse(error);
70+
71+
void reply.code(500).send(response);
72+
73+
return;
74+
}
75+
76+
// remove stack and message from error
77+
delete error.stack;
78+
error.message = message;
79+
80+
// let fastify handle the error
81+
throw error;
82+
};
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type { HttpErrors } from "@fastify/sensible";
2+
3+
declare module "fastify" {
4+
interface FastifyInstance {
5+
httpErrors: HttpErrors;
6+
stackTrace: boolean;
7+
}
8+
}
9+
10+
export { default } from "./plugin";
11+
12+
export { errorHandler } from "./errorHandler";
13+
14+
export { CustomError } from "./utils/error";
15+
16+
export type { HttpErrors } from "@fastify/sensible";
17+
18+
export type * from "./types";
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import fastifySensible from "@fastify/sensible";
2+
import FastifyPlugin from "fastify-plugin";
3+
4+
import { errorHandler } from "./errorHandler";
5+
6+
import type { ErrorHandlerOptions } from "./types";
7+
import type { FastifyInstance } from "fastify";
8+
9+
const plugin = async (
10+
fastify: FastifyInstance,
11+
options: ErrorHandlerOptions,
12+
) => {
13+
fastify.log.info("Registering fastify-error-handler plugin");
14+
15+
fastify.decorate("stackTrace", options.stackTrace || false);
16+
17+
await fastify.register(fastifySensible);
18+
19+
fastify.setErrorHandler(async (error, request, reply) => {
20+
if (options.preErrorHandler) {
21+
try {
22+
await options.preErrorHandler(error, request, reply);
23+
} catch {
24+
// If preErrorHandler throws an error, we can ignore it and continue
25+
}
26+
27+
if (reply.sent) {
28+
return;
29+
}
30+
}
31+
32+
return errorHandler(error, request, reply);
33+
});
34+
};
35+
36+
export default FastifyPlugin(plugin);
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { FastifyError, FastifyRequest, FastifyReply } from "fastify";
2+
3+
import type { StackFrame } from "stack-trace";
4+
5+
type ErrorHandler = (
6+
error: FastifyError,
7+
request: FastifyRequest,
8+
reply: FastifyReply,
9+
) => void | Promise<void>;
10+
11+
interface ErrorHandlerOptions {
12+
preErrorHandler?: ErrorHandler;
13+
stackTrace?: boolean;
14+
}
15+
16+
type ErrorResponse = {
17+
error?: string;
18+
code?: string;
19+
message: string;
20+
name: string;
21+
stack?: StackFrame[];
22+
statusCode: number;
23+
};
24+
25+
export type { ErrorHandler, ErrorHandlerOptions, ErrorResponse };
26+
27+
export { type StackFrame } from "stack-trace";
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export class CustomError extends Error {
2+
public code?: string;
3+
4+
constructor(message: string, code?: string) {
5+
super(message);
6+
7+
this.code = code;
8+
this.name = this.constructor.name; // sets name to "CustomError" so that it works in logs
9+
10+
// (error instanceof CustomError) and (error instanceof Error) both works because of this
11+
Object.setPrototypeOf(this, new.target.prototype);
12+
}
13+
}

0 commit comments

Comments
 (0)