- Регистрация
- 1 Мар 2015
- Сообщения
- 1,481
- Баллы
- 155
This article is loosely based off my recent video:
Hey everyone, Jason here ?
Let's talk about something that many JavaScript developers love to hate: try-catch.
In this article, I want to explore:
Consider this function:
function getUserAndPreference() {
try {
const user = getUser();
const preference = getUserPreference(user);
return [user, preference];
} catch (error) {
return null;
}
}
At first glance, this seems fine. But here's the issue: the try block catches all errors within it. That means if getUser or getUserPreference throws an error, or even if there's a typo elsewhere, they all get caught in the same catch block. This makes debugging and maintenance harder as your function grows.
To handle errors more granularly, you might consider wrapping each operation in its own try-catch:
function getUserAndPreference() {
let user: User;
try {
user = getUser();
} catch (error) {
return null;
}
let preference: UserPreference;
try {
preference = getUserPreference(user);
} catch (error) {
return null;
}
return [user, preference];
}
But this pattern has its own issues:
To summarize, the 3 pain-points of try-catch are:
To address these issues, many developers advocate for a "do not throw" approach. Instead of throwing errors, functions return a tuple containing the error and the result.
Here's a really simple example of such concept:
const tryCatch = async <T>(
promise: Promise<T>,
): Promise<[error: null, result: T] | [error: Error]> => {
try {
return [null, await promise];
} catch (error) {
return [error];
}
};
Usage:
const [error, user] = await tryCatch(getUser());
if (error) {
// handle error
}
This pattern mitigates all 3 issues mentioned in the previous section: allows for more granular error handling, allows for using const, and still keep TypeScript's type inference.
However, I find it not to be the most idiomatic way to JavaScript (handling errors as return values) and requires a utility function that's not standardized which causes never-ending discussions amongst engineers with different background and preferences.
Enter IIFE: Immediately Invoked Function Expression
IIFE, pronounced as Eevee (the Pokémon), is a pattern that's been around since the earliest days of JavaScript.
const user = await (async () => {
try {
return await getUser();
} catch (error) {
return null;
}
})();
By wrapping the operation in an IIFE, we can use try-catch as usual while eliminating all problems with try-catch mentioned previously.
We can apply the same pattern to getUserPreference:
const preference = await (async () => {
try {
return await getUserPreference(user);
} catch (error) {
return null;
}
})();
Now, our function looks like this:
async function getUserAndPreference() {
const user = await (async () => {
try {
return await getUser();
} catch (error) {
return null;
}
})();
if (!user) return null;
const preference = await (async () => {
try {
return await getUserPreference(user);
} catch (error) {
return null;
}
})();
if (!preference) return null;
return [user, preference];
}
This approach allows for granular error handling, maintains code integrity, and leverages type inference—all without any utility functions.
Looking Ahead: Do Expressions
There's a proposal for in JavaScript, currently at stage 1. It aims to bring similar functionality as IIFE but with cleaner syntax:
const getUserAndPreference = async () => {
const user = do {
try {
getUser();
} catch (error) {
// handle errors here
return null; // return from getUserAndPreference
}
};
// ...
};
Once this proposal is accepted as the language standard, try-catch IIFEs can migrate seamlessly to do expressions.
Final Thoughts
try-catch isn't inherently evil. It's how we use it that matters. By leveraging IIFE, we can write cleaner, more maintainable code without relying on external utilities or compromising code integrity.
I hope you found this helpful! If you did, please like, give me a follow, and share with your friends. If you have any questions or ideas, feel free to comment and let me know.
Let me know if you'd like to explore more patterns or have any questions!
Hey everyone, Jason here ?
Let's talk about something that many JavaScript developers love to hate: try-catch.
In this article, I want to explore:
- why try-catch can be frustrating
- discuss a common solution many have proposed, and
- introduce a pattern that I haven't seen enough people talk about
Consider this function:
function getUserAndPreference() {
try {
const user = getUser();
const preference = getUserPreference(user);
return [user, preference];
} catch (error) {
return null;
}
}
At first glance, this seems fine. But here's the issue: the try block catches all errors within it. That means if getUser or getUserPreference throws an error, or even if there's a typo elsewhere, they all get caught in the same catch block. This makes debugging and maintenance harder as your function grows.
To handle errors more granularly, you might consider wrapping each operation in its own try-catch:
function getUserAndPreference() {
let user: User;
try {
user = getUser();
} catch (error) {
return null;
}
let preference: UserPreference;
try {
preference = getUserPreference(user);
} catch (error) {
return null;
}
return [user, preference];
}
But this pattern has its own issues:
- Forced to use let instead of const This pattern requires us to declare the variables and assign them in different scope. This forces us to replace our "supposedly" const to let.
- Forced to use explicit typing In the previous example, all variables are typed automatically thanks to TypeScript's type inference. However, this approach forces us to seperate the declaring and assigning of the variables, meaning that TypeScript couldn't determine the variables types at declaration and use implicit any for those variables!
To summarize, the 3 pain-points of try-catch are:
- Catch-all behaviour
- Forcing let instead of const
- Compromising type inference by TypeScript
To address these issues, many developers advocate for a "do not throw" approach. Instead of throwing errors, functions return a tuple containing the error and the result.
Here's a really simple example of such concept:
const tryCatch = async <T>(
promise: Promise<T>,
): Promise<[error: null, result: T] | [error: Error]> => {
try {
return [null, await promise];
} catch (error) {
return [error];
}
};
Usage:
const [error, user] = await tryCatch(getUser());
if (error) {
// handle error
}
This pattern mitigates all 3 issues mentioned in the previous section: allows for more granular error handling, allows for using const, and still keep TypeScript's type inference.
However, I find it not to be the most idiomatic way to JavaScript (handling errors as return values) and requires a utility function that's not standardized which causes never-ending discussions amongst engineers with different background and preferences.
Enter IIFE: Immediately Invoked Function Expression
IIFE, pronounced as Eevee (the Pokémon), is a pattern that's been around since the earliest days of JavaScript.
const user = await (async () => {
try {
return await getUser();
} catch (error) {
return null;
}
})();
By wrapping the operation in an IIFE, we can use try-catch as usual while eliminating all problems with try-catch mentioned previously.
We can apply the same pattern to getUserPreference:
const preference = await (async () => {
try {
return await getUserPreference(user);
} catch (error) {
return null;
}
})();
Now, our function looks like this:
async function getUserAndPreference() {
const user = await (async () => {
try {
return await getUser();
} catch (error) {
return null;
}
})();
if (!user) return null;
const preference = await (async () => {
try {
return await getUserPreference(user);
} catch (error) {
return null;
}
})();
if (!preference) return null;
return [user, preference];
}
This approach allows for granular error handling, maintains code integrity, and leverages type inference—all without any utility functions.
Looking Ahead: Do Expressions
There's a proposal for in JavaScript, currently at stage 1. It aims to bring similar functionality as IIFE but with cleaner syntax:
const getUserAndPreference = async () => {
const user = do {
try {
getUser();
} catch (error) {
// handle errors here
return null; // return from getUserAndPreference
}
};
// ...
};
Once this proposal is accepted as the language standard, try-catch IIFEs can migrate seamlessly to do expressions.
Final Thoughts
try-catch isn't inherently evil. It's how we use it that matters. By leveraging IIFE, we can write cleaner, more maintainable code without relying on external utilities or compromising code integrity.
I hope you found this helpful! If you did, please like, give me a follow, and share with your friends. If you have any questions or ideas, feel free to comment and let me know.
Let me know if you'd like to explore more patterns or have any questions!