The topic of error handling in Javascript occurs to almost everyone. I should note that quite a lot of articles have already been written on this topic. That's why I won't go into details and basics. I will offer my own variant of error handling.
First, let me remind you that an error in Javascript is essentially “throwing an exception”. The exception needs to be handled by the application. By default, an exception is thrown by the Error object.
The traditional try/catch approach has its disadvantages: lack of strict typing in the catch block, confusion between technical failures and business logic errors, and frequent cases when developers forget to handle errors in critical places. This problem can be solved by using the Either pattern, which allows you to explicitly separate successful and erroneous results. In this article, we will describe how to implement error handling in TypeScript by creating custom exceptions and show how a functional approach to handling errors can be used using the Monet library.
1. Error handling in the standard way
The usual way of handling errors is to use try/catch blocks. However, the catch block lacks error typing (the default type is any), which may lead to difficulties in further analysis of the exception. Besides, it is difficult to separate technical errors (for example, database connection errors) from business logic errors.
To solve this problem, custom errors are often written. Here is an example of creating a custom error:
class OrderError extends Error {
constructor(code: string, message: string) {
super(message);
this.name = "OrderError";
this.code = code; // insufficient_funds, invalid_payment, incorrect_id etc
const codeMessageMap = {
'insufficient_funds': 'You do not have enough funds to complete this order',
'invalid_payment': 'There was an error during last payment',
'incorrect_id': 'Wrong ID was provided',
}
this.userMessage = codeMessageMap[code] || "An error occurred with your order"
}
}
function processOrder(orderId: number): void {
if (orderId <= 0) {
throw new OrderError("incorrect_id", "Wrong ID");
}
}
This approach allows you to define the error type more explicitly, but the catch block itself continues to work with the any type, which is not always convenient.
Here is how error handling will look like in this case:
try{
processOrder(2)
}catch(err){
if(err instanceof OrderError){
if(err.code === 'incorrect_id'){
alert(err.userMessage)
console.log(err.message);
}
if(err.code === 'invalid_payment'){
// show message to user
}
} else{
alert('Something went wrong! Please contact support')
console.log(err.message);
}
}
Javascript errors also support nesting. In the example below, we catch an error, wrap it in our own, preserving the context and adding a clearer description.
function displayMoney(cents){
try{
document.querySelector("#money").innerText = formatMoney(cents)
}catch(err){
if(err.message.includes('innerText')){
throw new Error('Div with the id of money is missing from the page', {
cause: err
})
}
throw err;
}
}
It is often difficult to distinguish technical errors from business logic errors. Consider the following example, where we throw an error after processing an API request, thus at the level above we can't accurately understand whether the problem is technical or business logic.
async function fetchWithRetry(url, attempts = 3){
while(attempts > 0){
try{
const res = await fetch(url)
if(res.ok){
return res
}else{
throw new Error("Network response was not ok")
}
}catch(err){
attempts--;
if(attempts === 0){
throw err
}
}
}
}
try{
await fetchWithRetry("https://httpbin.org/status/200,500")
}catch(err){
console.log("caught retry example", err)
}
console.log("end!")
2. Either principle and advantages of functional error handling
The Either pattern is widely used in functional programming. Its essence is that a function returns not an exception but a result, which can be either successful (Right) or erroneous (Left). This approach is similar to error handling in Go, where errors are returned along with the result rather than being thrown away.
Advantages of Either:
- Clear separation between a successful outcome and an error.
- Ability to compose multiple functions without the need for multiple try/catch blocks.
- Transparent error handling - the result explicitly reflects whether the operation is successful or not.
3. Example of error handling implementation using Either
In this example, we implement the function of updating an email client in CRM. To work with Either, we use the Monet library. If during the update it is detected that the client does not exist or an incorrect email is passed, the function returns Either.left with the corresponding error. Otherwise - Either.right with updated client data.
Code example:
import { Either } from "monet";
interface Client {
id: number;
name: string;
email: string;
}
class NotFoundError extends Error {
constructor(code: int, message: string) {
super(message);
this.name = "NotFoundError";
this.code = code
}
}
class ValidationError extends Error {
constructor(field: string, message: string) {
super(message);
this.name = "BusinessError";
this.field = field;
}
}
const fakeDatabase: Client[] = [
{ id: 1, name: "Иван Иванов", email: "ivan@example.com" },
{ id: 2, name: "Мария Петрова", email: "maria@example.com" }
];
function updateClientEmail(clientId: number, newEmail: string): Either<Error, Client> {
const client: Client | undefined = fakeDatabase.find(client => client.id === clientId);
if (!client) {
return Either.Left(new NotFoundError(404, `Клиент с id=${clientId} не найден`));
}
if (!newEmail.includes("@")) {
return Either.Left(new BusinessError("email", "Wrong email provided"));
}
client.email = newEmail;
return Either.Right(client);
}
// How to use it
const result = updateClientEmail(1, "newemail@example.com");
result.cata(
(error) => console.error("Ошибка при обновлении клиента:", error.message),
(client) => console.log("Клиент успешно обновлен:", client)
);
In this example:
- The updateClientEmail function returns Either.
- If an error occurs (for example, the client is not found or the email is incorrect), it returns Either.Left with the error object.
- If everything was successful, Either.Right with the updated client object is returned.
- The cata method allows you to “unwrap” Either and execute one of the functions, depending on a successful or erroneous result.
4. Explanation and opportunities for further improvement
How Either works:
- The left option (Left) represents an error and the right option (Right) represents a successful result.
- The use of flatMap, map and cata methods makes it easy to compose a sequence of operations without resorting to nested try/catch.
Further areas of improvement:
- It is possible to extend functionality by implementing methods to support composition (e.g. flatMap) in order to combine multiple functions that return Either.
- If the fp-ts library is used, it is possible to further enrich error handling with already ready-made monadic constructs.
- Adding logging and automated error auditing to CRM will improve business process monitoring.
Conclusion
Error handling is a key aspect of developing stable and scalable CRM systems. Traditional try/catch does not always provide the necessary typing and separation of errors, which can lead to complexity in code maintenance. Using the Either pattern with the Monet library demonstrates how error handling can be made declarative, easily componentized, and robust. The example above shows how a client data update function can be implemented by explicitly separating successful outcomes and error situations. This functional approach simplifies the scaling and integration of system components and makes the code more transparent and manageable.