Transform Async/Await Error Handling with Go‑Style Patterns in JavaScript

This article explains how the traditional try...catch approach for async/await can lead to nested, hard‑to‑read code and introduces a Go‑style error‑handling helper that returns [error, data] tuples, flattening logic, reducing boilerplate, and improving readability when combined with Promise.all.

JavaScript
JavaScript
JavaScript
Transform Async/Await Error Handling with Go‑Style Patterns in JavaScript
async/await

is ES7 syntax sugar that lets asynchronous code be written in a seemingly synchronous way, greatly improving readability and maintainability.

However, the convenience of async/await often brings back an "old friend": try...catch, which must wrap each await to catch rejected promises.

Root problem: ubiquitous try...catch

To capture a rejected Promise we must place the code inside a try...catch block. For example, fetching user information from a server:

import { fetchUserById } from './api';
async function displayUser(userId) {
  try {
    const user = await fetchUserById(userId);
    console.log('用户信息:', user.name);
    // ... more operations based on user
  } catch (error) {
    console.error('获取用户失败:', error);
  }
}

When business logic becomes slightly more complex—such as needing to request multiple APIs sequentially—the code quickly turns into deeply nested try...catch blocks:

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 nesting causes three obvious problems: code redundancy, poor readability, and mixed concerns.

Elegant solution: Go‑style error handling

In Go, functions typically return two values— result and error —and callers check whether error is nil. We can bring this idea into JavaScript by creating a helper function to that always resolves to an array [error, data].

If the Promise resolves, to returns [null, data].

If the Promise rejects, to returns [error, null].

Implementation of to (pure JavaScript version shown in the image):

Using to, the previous displayUser function can be rewritten without any try...catch:

The to function is tiny but powerful; it encapsulates the try...catch logic internally and exposes a uniform, flat interface.

Refactoring with to

After refactoring, the function contains no try...catch, uses guard clauses to handle errors early, and leaves the happy path as clear, top‑level code.

No try...catch – the function body becomes flat.

Error‑first handling – an if statement checks and processes the error, then returns early.

High readability – after error handling, only the success logic remains, without any nesting.

Applying the same pattern to the complex loadPageData transforms it into a linear, predictable flow:

Advantages of the new pattern

Code becomes flatter and clearer by eliminating nested try...catch blocks.

Reduces boilerplate by encapsulating error handling in the reusable to function.

Forces explicit error handling through destructuring const [error, data], making missed errors unlikely.

Separates concerns: guard clauses isolate error handling from success logic, improving maintainability.

Combining with Promise.all

The pattern works equally well for concurrent requests. By pairing Promise.all with to, you can handle partial failures without losing successful results:

async function loadDashboard(userId) {
  const [
    [userError, userData],
    [settingsError, settingsData]
  ] = await Promise.all([
    to(fetchUser(userId)),
    to(fetchUserSettings(userId))
  ]);

  if (userError) {
    console.error('加载用户数据失败');
  }
  if (settingsError) {
    console.error('加载用户设置失败');
  }
  // Even if one fails, the other successful data is still usable
  if (userData) {
    // ... use userData
  }
  if (settingsData) {
    // ... use settingsData
  }
}

Using Promise.all with to lets you gracefully handle scenarios where some promises succeed and others fail, whereas a plain try...catch would abort on the first rejection and discard all results.

In summary, try...catch remains the cornerstone of JavaScript error handling, but abstracting it into a reusable to helper yields cleaner, more maintainable asynchronous code.

JavaScriptError Handlingasync/awaitPromiseGo styleto function
JavaScript
Written by

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.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.