Best practices for typesafe error handling

What are your best practices for error handling using typescript? Are you throwing (custom?) errors or do you return success/error union types? Or do you do something else?
11 Replies
Unknown User
Unknown Userā€¢15mo ago
Message Not Public
Sign In & Join Server To View
Patrick
Patrickā€¢15mo ago
Thanks for your insights. I'm not sure if I'm a fan of custom error classes in a typescript world where more and more libraries do a functional approach. Creating multiple classes with different fields seems tedious in contrast to defining some union types which could be implemented as discriminating unions. That's why I'm asking for best practices. I can't find any good resources on this topic šŸ™‚
Unknown User
Unknown Userā€¢15mo ago
Message Not Public
Sign In & Join Server To View
Patrick
Patrickā€¢15mo ago
I've seen nevertrow and have thought about using it for our team. What's your opinion on it after using it?
scot22
scot22ā€¢15mo ago
I throw custom errors, we used to heavily use monads and error return types in the past but in js it just makes it more complicated Mainly because third party libraries and tools either throw errors or expect you to throw errors And onboarding new people and explaining that you have three different ways of handling errors just makes it tough So we opted to use async await everywhere and try catch
jingleberry
jingleberryā€¢15mo ago
I once went ham on fp-ts and use monads for everything just to avoid having to use a try catch statement. The result - spent more time writing flawless code instead of creating value Thankfully this was just for a personal project.
Patrick
Patrickā€¢15mo ago
Thanks for your insights, that's really helpful. But it seems like there is no definitive answer to this (like a best practice) and we'll have to see whats best for us. It's really hard to find good resources on this topic. Interestingly trpc returns an error object and doesn't throw an error you have to catch. It seems the topic is handled really inconsistently in the ts-ecosystem.
Sybatron
Sybatronā€¢15mo ago
interface CustomError<ErrorCode> extends Error {
code: ErrorCode;
data: DataType;
}

type DataType = any;

const createError = <ErrorCode>(
message: string,
code: ErrorCode,
data: DataType
): CustomError<ErrorCode> => {
const error: CustomError<ErrorCode> = new Error(
message
) as CustomError<ErrorCode>;
error.code = code;
error.data = data;
return error;
};

type ErrorCode = 400 | 404 | 500;
// type ErrorCode = number;

const myFunction = (param: string) => {
if (!param) {
throw createError<ErrorCode>("Parameter is required", 400, {
var1: "string",
var2: "2",
var3: 25,
});
}

// rest of the function
};
interface CustomError<ErrorCode> extends Error {
code: ErrorCode;
data: DataType;
}

type DataType = any;

const createError = <ErrorCode>(
message: string,
code: ErrorCode,
data: DataType
): CustomError<ErrorCode> => {
const error: CustomError<ErrorCode> = new Error(
message
) as CustomError<ErrorCode>;
error.code = code;
error.data = data;
return error;
};

type ErrorCode = 400 | 404 | 500;
// type ErrorCode = number;

const myFunction = (param: string) => {
if (!param) {
throw createError<ErrorCode>("Parameter is required", 400, {
var1: "string",
var2: "2",
var3: 25,
});
}

// rest of the function
};
šŸ¤” correct me and give feedback if I'm wrong šŸ˜… still new to typescript
jingleberry
jingleberryā€¢15mo ago
This is just because tools like react query return data, isLoading and error objects. You can opt into using suspense which will also result in throwing errors to be caught by an error boundary
Patrick
Patrickā€¢15mo ago
Thank you I'll have to take another look at that. šŸ‘
denolfe
denolfeā€¢15mo ago
A few things to add to this if you're making custom errors: - You'll want to take advantage of the cause property in the second parameter of the Error constructor to wrap the original error. This will preserve the original stack trace. - Be sure to set the prototype properly with Object.setPrototypeOf(this, MyCustomError.prototype). This will allow you to use if (error instanceof MyCustomError) Here is an example of using the above:
type ErrorCode =
| 'OTHER'
| 'SOME_FAILURE'
| 'ANOTHER_TYPE_OF_FAILURE'

export interface MyCustomErrorProps {
message: string
/** Original error */
cause?: unknown
code?: ErrorCode
}

export class MyCustomError extends Error {
code: ErrorCode
constructor(props: MyCustomErrorProps) {
super(props.message, (props.cause instanceof Error && { cause: props.cause }) || undefined)
this.name = this.constructor.name
this.code = props.code || 'OTHER'

Error.captureStackTrace(this)
Object.setPrototypeOf(this, MyCustomError.prototype)
}
}
type ErrorCode =
| 'OTHER'
| 'SOME_FAILURE'
| 'ANOTHER_TYPE_OF_FAILURE'

export interface MyCustomErrorProps {
message: string
/** Original error */
cause?: unknown
code?: ErrorCode
}

export class MyCustomError extends Error {
code: ErrorCode
constructor(props: MyCustomErrorProps) {
super(props.message, (props.cause instanceof Error && { cause: props.cause }) || undefined)
this.name = this.constructor.name
this.code = props.code || 'OTHER'

Error.captureStackTrace(this)
Object.setPrototypeOf(this, MyCustomError.prototype)
}
}