Can You Replace try…catch with a Go‑Style Helper in JavaScript?
This article explains how async/await simplifies asynchronous JavaScript but still forces repetitive try…catch blocks, and introduces a Go‑inspired helper function that returns an [error, data] tuple to flatten error handling, improve readability, and work seamlessly with Promise.all.
Async/await, introduced in ES7, lets developers write asynchronous JavaScript in a seemingly synchronous style, greatly improving readability. However, handling rejected promises still requires wrapping each await in a try...catch block, which quickly leads to nested, repetitive error‑handling code.
Root cause: ubiquitous try...catch
To catch a rejected promise after an await, the code must be placed inside a try...catch block. A simple example fetching user information looks like this:
import { fetchUserById } from './api';
async function displayUser(userId) {
try {
const user = await fetchUserById(userId);
console.log('用户信息:', user.name);
// ... more operations on user
} catch (error) {
console.error('获取用户失败:', error);
// ... error handling, e.g., show a toast
}
}When business logic grows—e.g., fetching posts and comments sequentially—the code becomes heavily nested:
async function loadPageData(userId) {
try {
const user = await fetchUserById(userId);
console.log('用户信息:', user.name);
try {
const posts = await fetchPostsByUserId(user.id);
console.log('用户文章:', posts);
try {
const comments = await fetchCommentsForPosts(posts[0].id);
console.log('文章评论:', comments);
} catch (commentError) {
console.error('获取评论失败:', commentError);
}
} catch (postError) {
console.error('获取文章失败:', postError);
}
} catch (userError) {
console.error('获取用户失败:', userError);
}
}This pattern suffers from three main drawbacks:
Code redundancy : each async step repeats a try...catch structure, inflating boilerplate.
Poor readability : the “happy path” is buried inside multiple try blocks, disrupting the natural flow.
Mixed concerns : success logic and error handling are tightly coupled, reducing function cohesion.
Elegant solution: Go‑style error handling
Go functions typically return two values— result and error —and callers check whether error is nil. We can emulate this in JavaScript by creating a helper that accepts a Promise and always resolves to a two‑element array [error, data]:
If the promise resolves, the helper returns [null, data].
If the promise rejects, it returns [error, null].
The helper, named to, never throws; it encapsulates the try...catch internally.
If you are not using TypeScript, the pure JavaScript implementation looks like this (illustrated in the next image):
Refactoring with to
Using to, the earlier displayUser function becomes:
The transformation yields three clear benefits:
No more try...catch : the function body is flat and easy to scan.
Error‑first handling : a guard clause checks if (error) and returns early, keeping success logic separate.
High readability : after error handling, the remaining code is the core happy path, with no additional indentation.
Applying the same pattern to the complex loadPageData example produces a linear flow where each step’s error is handled independently, eliminating the “callback hell” of nested try...catch blocks.
Combining with Promise.all
The pattern works equally well with concurrent requests. By wrapping each fetch with to and feeding the results to Promise.all, you can handle partial failures without losing successful data:
async function loadDashboard(userId) {
const [
[userError, userData],
[settingsError, settingsData]
] = await Promise.all([
to(fetchUser(userId)),
to(fetchUserSettings(userId))
]);
if (userError) {
console.error('加载用户数据失败');
// handle user error
}
if (settingsError) {
console.error('加载用户设置失败');
// handle settings error
}
// Even if one fails, the other data remains usable
if (userData) {
// ...process userData
}
if (settingsData) {
// ...process settingsData
}
}Using Promise.all together with to lets you gracefully process multiple promises where some succeed and others fail, whereas a plain try...catch would abort on the first rejection and discard all results.
Conclusion
The traditional try...catch remains the foundation of JavaScript error handling, but by abstracting it into a reusable to helper you can eliminate repetitive boilerplate, keep error handling explicit, and write flatter, more maintainable asynchronous code.
JavaScript
Provides JavaScript enthusiasts with tutorials and experience sharing on web front‑end technologies, including JavaScript, Node.js, Deno, Vue.js, React, Angular, HTML5, CSS3, and more.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
