1
18
19export class FatalError extends Error {
20 constructor(
21 public readonly code: string,
22 message: string
23 ) {
24 super(message);
25 this.name = "FatalError";
26 }
27}
28
29export class RetryableError extends Error {
30 constructor(
31 public readonly code: string,
32 message: string
33 ) {
34 super(message);
35 this.name = "RetryableError";
36 }
37}
38
39
40const RETRYABLE_ERRNO = new Set(["ECONNRESET", "ETIMEDOUT", "EPIPE", "ECONNREFUSED", "EAI_AGAIN"]);
41
42
43const RETRYABLE_PG = new Set(["40P01", "40001", "57P01", "55P03", "57014"]);
44
45
52export function classifyError(err: unknown): { retryable: boolean; code: string } {
53 if (err instanceof FatalError) return { retryable: false, code: err.code };
54 if (err instanceof RetryableError) return { retryable: true, code: err.code };
55
56 const e = err as { code?: string; errno?: string; cause?: { errno?: string }; meta?: { driverError?: { code?: string } } };
57 const pgCode =
58 e?.meta?.driverError?.code ??
59 (typeof e?.code === "string" && RETRYABLE_PG.has(e.code) ? e.code : undefined);
60 if (pgCode && RETRYABLE_PG.has(pgCode)) return { retryable: true, code: `DB_TRANSIENT_${pgCode}` };
61
62 const errno = e?.errno ?? e?.cause?.errno;
63 if (errno && RETRYABLE_ERRNO.has(errno)) return { retryable: true, code: `NET_${errno}` };
64 if (typeof e?.code === "string" && RETRYABLE_ERRNO.has(e.code)) return { retryable: true, code: `NET_${e.code}` };
65
66 return { retryable: false, code: (err as Error)?.name === "TimeoutError" ? "ACTION_TIMEOUT" : "UNEXPECTED" };
67}
68