I remember reading that error handling and meaningful logging are the most common forgotten areas for programmers who aim to improve their skills.
Like, these two are really crucial elements, yet, receive minimal attention π€·ββοΈ.
We - me included - prefer the happy path.
I mean, it's the happy π path.
Lets review Custom Exceptions in modern javascript / typescript.
Starting point.
β We have the Error built-in object.
β Using the
Error()constructor we get an object with 3 properties
(copied from typescript/lib.es5.d.ts):
interface Error {
name: string;
message: string;
stack?: string;
}
note:
linenumberandfilenameare exclusive to Mozilla, that's why those aren't defined atlib.es5.d.ts
β There are other built-in constructors available like
TypeError()orSyntaxError()β key property that differs an
Errorfrom aTypeError(for instance) isname:
Our goal is:
- To be able to define custom errors of our domain
- So we're able to detect such errors
- Access custom properties, extending the defaults like
stack
In pseudo-code:
try {
foo();
} catch(err) {
if (/* err is from my Domain so it has custom properties */) {
const value = err. /* ts is able to suggest properties */
...
...
Solution.
Let's pretend we've defined that our domain has AuthError for authentication issues and OperationUnavailableError for other logic issues related to our model.
modern Js Solution π¨.
- Just a function that creates a regular
Error - Define the
name(remember, this property is used to differentiate among Errors). - Define any extra properties
function AuthError(msg) {
const err = Error(msg);
err.name = 'AuthError';
err.userId = getCurrentUserId();
return err;
}
Raising it:
function authenticate() {
...
throw AuthError('user not authorized')
...
}
Check that we are keeping all the default value from built-in Error:
And catching it:
try {
authenticate();
} catch(err) {
if (err.name === 'AuthError') {
const { userId } = err;
...
}
...
}
Note: I intentionally avoid using the class keyword; it's so Java-ish doesn't it?
Ts Solution π¦
Repeating the same here , but including type annotations:
First, the error types.
interface AuthError extends Error {
name: "AuthError";
userId: string;
}
interface OperationUnavailableError extends Error {
name: "OperationUnavailableError";
info: Record<string, unknown>;
}
this is one of those rare cases where i prefer
interfacetotypesince we're extending a built-in interface
And the constructor functions:
function AuthError(msg: string) {
const error = new Error(msg) as AuthError;
error.name = "AuthError";
error.userId = getCurrentUserId();
return error;
}
function OperationUnavailableError(msg: string) {
const error = new Error(msg) as OperationUnavailableError;
error.name = "OperationUnavailableError";
error.info = getOperationInfo();
return error;
}
Raising it:
function authenticate() {
...
throw AuthError('user not authorized')
...
}
and catching them...
π€
Using Type Guards β
Including these type guards will make your custom errors even nicer:
the devil is in the details
function isAuthError(error: Error): error is AuthError {
return error.name === "AuthError";
}
function isOperationUnavailableError(
error: Error
): error is OperationUnavailableError {
return error.name === "OperationUnavailableError";
}
Code examples mixing up the thing:
My final advice: Don't over-use custom domain errors; too many can lead to a bureaucratic pyramid of definitions.
They are like... Tabasco πΆοΈ.
A touch of Tabasco can enhance your code, but moderation is key. If you opt for custom domain errors, keep them simple, following the approach presented here.
thanks for reading π.





Top comments (4)
Why not extend the base
Errorclass?E.g.:
an option for sure! this is a personal mania of avoiding
classand using just the actual function.The rationale is
composition >>>>>>>>>>>>>>>>>>>>>...>>>>>>> inheritanceand
classleads inevitably toextendsabstractthisprotectedwhich βIMOβ makes the software harder to read and maintain.THIS is a big topic and im trying to preapre a big article on its own actually!
TL;DR: I βpersoanllyβ avoid
classat any cost (and prefer plainfunction) but this solution is completely valid! πI would think, that this is an example, where inheritance leads to simpler and easier to understand code.
Especially since the
Errortype is used as an interface (some might also call it atraitπ¦), which is a cornerstone of composability.inheritance leads to simpler and easier to understand code. this sentence is just.... π ββοΈ
subjective I know.
I just avoid inheritance like the plague π