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:
linenumber
andfilename
are 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
Error
from 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
interface
totype
since 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 (0)