I’m trying to handle exceptions thrown by NodeJS using TypeScript.
For example, if I get an ECONNREFUSED
exception, it’s of type SystemError
.
But as of now, because err
is of type any
, I can only do this
redisClient.on('error', (err) => {
console.log('Error: ', err);
});
But I would like to narrow down the error type, similar to this:
redisClient.on('error', (err) => {
if (err instanceof SystemError) {
if(err.code === 'ECONNREFUSED ') {
throw new Error('Connection refused');
}
}
throw err;
});
But unfortunately, SystemError
is not exported in Node.
I did find a discussion on GitHub from 4 years ago regarding NodeJS.ErrnoException
, but seems like it was removed.
Is it currently possible in NodeJS?
Note: This is also discussed at the GitHub issue nodejs/node#46869 — Export SystemError
I needed to discriminate system errors in the type system in some past projects and didn’t find any packages that did so in a way that was satisfactory, so I wrote a simple, extensible module that I’ve been using since then — it defines some interfaces from information in the Node.js documentation (links to source included) and type guards which use those interfaces:
system_error.ts
:
import { getSystemErrorMap } from "node:util";
import { isNativeError } from "node:util/types";
/** https://nodejs.org/docs/latest-v20.x/api/errors.html#class-systemerror */
export interface BaseSystemError<Code extends string = string> extends Error {
/** The string error code */
code: Code;
/** The system-provided error number */
errno: number;
/** A system-provided human-readable description of the error */
message: string;
/** The name of the system call that triggered the error */
syscall: string;
}
/** https://nodejs.org/docs/latest-v20.x/api/errors.html#class-systemerror */
export interface SystemError<Code extends string = string>
extends BaseSystemError<Code> {
/** If present, the address to which a network connection failed */
address?: string;
/** If present, the file path destination when reporting a file system error */
dest?: string;
/** If present, extra details about the error condition */
info?: Record<string, unknown>;
/** If present, the file path when reporting a file system error */
path?: string;
/** If present, the network connection port that is not available */
port?: number;
}
// Memo (lazily initialized):
// https://nodejs.org/docs/latest-v20.x/api/util.html#utilgetsystemerrormap
let systemErrorMap: ReturnType<typeof getSystemErrorMap> | undefined;
type JsType =
| "bigint"
| "boolean"
| "function"
| "number"
| "object"
| "string"
| "symbol"
| "undefined";
export function isSystemError(value: unknown): value is SystemError {
// https://nodejs.org/docs/latest-v20.x/api/util.html#utiltypesisnativeerrorvalue
if (!isNativeError(value)) return false;
for (const [key, jsType] of [
["code", "string"],
["errno", "number"],
["syscall", "string"],
] satisfies [keyof SystemError, JsType][]) {
if (typeof (value as SystemError)[key] !== jsType) return false;
}
systemErrorMap ??= getSystemErrorMap();
return systemErrorMap.has((value as SystemError).errno);
}
export function isSystemErrorWithCode<T extends string>(
value: unknown,
code: T,
): value is SystemError<T> {
return isSystemError(value) && value.code === code;
}
// You can also define type guards for any specific system errors that you want to discriminate…
// https://nodejs.org/docs/latest-v20.x/api/errors.html#common-system-errors
export function isConnectionRefusedError(
value: unknown,
): value is SystemError<"ECONNREFUSED"> {
return isSystemErrorWithCode(value, "ECONNREFUSED");
}
export function isNotFoundError(
value: unknown,
): value is SystemError<"ENOENT"> {
return isSystemErrorWithCode(value, "ENOENT");
}
// …etc.
You can use it in your code like this:
import { isConnectionRefusedError } from "./system_error"
function example() {
try {
// statements…
} catch (cause) {
if (isConnectionRefusedError(cause)) {
cause
//^? SystemError<"ECONNREFUSED">
cause.code
// ^? BaseSystemError<"ECONNREFUSED">.code: "ECONNREFUSED"
}
}
}
// or the Redis example in your question:
redisClient.on("error", (err) => {
if (isConnectionRefusedError(err)) {
// It's a "connection refused" error; do something…
} else {
// do something different…
}
});
Code in the TypeScript Playground
Pls try this to handle errors in Typescript:
interface NodeSystemError extends Error {
code?: string;
errno?: number;
syscall?: string;
path?: string;
address?: string;
port?: number;
}
redisClient.on('error', (err) => {
const error = err as NodeSystemError;
if (error.code === 'ECONNREFUSED') {
console.error('Connection refused:', error);
throw new Error('Redis connection refused');
} else if (error.code === 'EHOSTUNREACH') {
console.error('Host unreachable:', error);
throw new Error('Redis host unreachable');
} else {
console.error('Unhandled error:', error);
throw error;
}
});
we can also use instanceof
redisClient.on('error', (err) => {
if (err instanceof Error && (err as any).code === 'ECONNREFUSED') {
console.error('Connection refused:', err);
throw new Error('Redis connection refused');
} else if (err instanceof Error && (err as any).code === 'EHOSTUNREACH') {
console.error('Host unreachable:', err);
throw new Error('Redis host unreachable');
} else {
console.error('Unhandled error:', err);
throw err;
}
});
2