UNPKG

6.47 kBJavaScriptView Raw
1// @ts-nocheck
2
3import { inspect } from "node:util";
4import { isNil, isPlainObject } from "./lodash.js";
5
6/**
7 * Standard error to use. This contains a key, status code and info object.
8 * Mostly provided to make it easier to return errors from your API's.
9 *
10 * @since 0.1.0
11 * @class
12 */
13export class AppError extends Error {
14 /**
15 * @param {string} key
16 * @param {number} status
17 * @param {Record<string, any>} [info={}]
18 * @param {Error} [cause]
19 */
20 constructor(key, status, info, cause) {
21 super();
22
23 this.key = key;
24 this.status = status;
25 this.info = info || {};
26 this.cause = cause;
27
28 Object.setPrototypeOf(this, AppError.prototype);
29
30 if (typeof status !== "number" || typeof key !== "string") {
31 return AppError.serverError(
32 {
33 appErrorConstructParams: {
34 key,
35 status,
36 },
37 },
38 this,
39 );
40 }
41 }
42
43 /**
44 * @param {*} value
45 * @returns {value is AppError}
46 */
47 static instanceOf(value) {
48 return (
49 value &&
50 typeof value.key === "string" &&
51 typeof value.status === "number" &&
52 !!value.info
53 );
54 }
55
56 /**
57 * @param {Record<string, any>} [info={}]
58 * @param {Error} [error]
59 * @returns {AppError}
60 */
61 static notFound(info = {}, error = undefined) {
62 return new AppError("error.server.notFound", 404, info, error);
63 }
64
65 /**
66 * @param {Record<string, any>} [info={}]
67 * @param {Error} [error]
68 * @returns {AppError}
69 */
70 static notImplemented(info = {}, error = undefined) {
71 return new AppError("error.server.notImplemented", 405, info, error);
72 }
73
74 /**
75 * @param {Record<string, any>} [info={}]
76 * @param {Error} [error]
77 * @returns {AppError}
78 */
79 static serverError(info = {}, error = undefined) {
80 return new AppError("error.server.internal", 500, info, error);
81 }
82
83 /**
84 * @param {string} key
85 * @param {Record<string, any>} [info={}]
86 * @param {Error} [error]
87 * @returns {AppError}
88 */
89 static validationError(key, info = {}, error = undefined) {
90 return new AppError(key, 400, info, error);
91 }
92
93 /**
94 * Format any error skipping the stack automatically for nested errors
95 *
96 * @param {AppError | Error | undefined | null | {} | string | number | boolean |
97 * Function | unknown} [e]
98 * @returns {Record<string, any>}
99 */
100 static format(e) {
101 if (isNil(e)) {
102 return {
103 warning: "Missing error",
104 };
105 }
106
107 const typeOf = typeof e;
108
109 if (typeOf === "symbol") {
110 return {
111 warning: "Can't serialize Symbol",
112 };
113 }
114
115 if (typeOf === "bigint") {
116 return {
117 warning: "Can't serialize BigInt",
118 };
119 }
120
121 if (typeOf === "string" || typeOf === "boolean" || typeOf === "number") {
122 return {
123 value: e,
124 };
125 }
126
127 if (typeOf === "function") {
128 return {
129 type: "function",
130 name: e.name,
131 parameterLength: e.length,
132 };
133 }
134
135 let stack;
136 if (typeof (e?.stack ?? "") === "string") {
137 stack = (e?.stack ?? "").split("\n").map((it) => it.trim());
138 // Remove first item as this is the Error name
139 stack.shift();
140 } else if (Array.isArray(e?.stack)) {
141 stack = e?.stack;
142 }
143
144 if (isNil(e)) {
145 return e;
146 } else if (AppError.instanceOf(e)) {
147 if (Array.isArray(e.stack)) {
148 // Already formatted error
149 return e;
150 }
151
152 return {
153 key: e.key,
154 status: e.status,
155 info: e.info,
156 stack,
157 cause: e.cause ? AppError.format(e.cause) : undefined,
158 };
159 } else if (e.name === "AggregateError") {
160 return {
161 name: e.name,
162 message: e.message,
163 stack,
164 cause: e.errors?.map((it) => AppError.format(it)),
165 };
166 } else if (e.name === "PostgresError") {
167 return {
168 name: e.name,
169 message: e.message,
170 postgres: {
171 severity: e?.severity,
172 code: e?.code,
173 position: e?.position,
174 routine: e?.routine,
175 severity_local: e?.severity_local,
176 file: e?.file,
177 line: e?.line,
178
179 detail: e?.detail,
180 hint: e?.hint,
181 internal_position: e?.internal_position,
182 internal_query: e?.internal_query,
183 where: e?.where,
184 schema_name: e?.schema_name,
185 table_name: e?.table_name,
186 column_name: e?.column_name,
187 data: e?.data,
188 type_name: e?.type_name,
189 constraint_name: e?.constraint_name,
190
191 query: e?.query,
192 parameters: e?.parameters,
193 },
194 stack,
195 };
196 } else if (e.isAxiosError) {
197 // Dumping is most of the time not what the user expects, so prevent these from
198 // being added to the result.
199 const body =
200 typeof e.response?.data?.pipe === "function" && // @ts-ignore
201 typeof e.response?.data?._read === "function"
202 ? {
203 message:
204 "Response was a stream, which can not be serialized by AppError#format. Use a try-catch and 'streamToBuffer(e.response?.data)' to get the provided response.",
205 }
206 : e.response?.data;
207
208 return {
209 name: e.name,
210 message: e.message,
211 axios: {
212 request: {
213 path: e.request?.path,
214 method: e.request?.method,
215 baseUrl: e.request?.baseURL,
216 },
217 response: {
218 status: e.response?.status,
219 body,
220 },
221 },
222 stack,
223 };
224 } else if (typeof e.toJSON === "function") {
225 const result = e.toJSON();
226 result.stack = stack;
227 return result;
228 } else if (isPlainObject(e)) {
229 if (isNil(e.stack)) {
230 e.stack = stack;
231 }
232 return e;
233 }
234
235 // Any unhandled case
236 return {
237 name: e.name,
238 message: e.message,
239 stack,
240 cause: e.cause ? AppError.format(e.cause) : undefined,
241 };
242 }
243
244 /**
245 * Use AppError#format when AppError is passed to console.log / console.error.
246 * This works because it uses `util.inspect` under the hood.
247 * Util#inspect checks if the Symbol `util.inspect.custom` is available.
248 */
249 [inspect.custom]() {
250 return AppError.format(this);
251 }
252
253 /**
254 * Use AppError#format when AppError is passed to JSON.stringify().
255 * This is used in the compas insight logger in production mode.
256 */
257 toJSON() {
258 return AppError.format(this);
259 }
260}