/
/
Try-catch; The GOTO that Needs to GOAWAY
6
min read

Try-catch; The GOTO that Needs to GOAWAY

Rethinking JavaScript error handling by replacing try-catch with Go-inspired patterns. Cleaner, safer, and more readable code without the chaos.

"Do I wrap my very specific code with a try-catch and move the variable creation outside the scope, or do I just keep expanding the catch scope as I go?"

We’ve all been there—balancing clean, readable code with the messy reality of error handling. On one hand, we want to avoid cluttering our logic with boilerplate. On the other, we don't want to deal with awkward scoping issues or argue with the linter about whether a variable exists. So, what’s the “right” answer? Well... there sort of is one. But it’s not found in JavaScript. Or C#.

It’s found in Go.

But how does a language remove the need for something as deeply embedded in frontend and backend development as try-catch?

If you ask me, try-catch is like the plague. It’s essentially a glorified GOTO statement,1 just dressed up in structured programming syntax. When an error is thrown, execution jumps—often unpredictably—to a separate block of code, potentially far removed from the scope in which the error occurred. This makes following the logical flow of a program harder, and managing side effects even trickier.

When I first encountered try-catch, coming from a background in batch scripting—where GOTO was king—it seemed like a programming savior. It caught errors! It let me ignore them! It gave me the illusion of control. But eventually, I realized that it was adding cognitive load and making my code harder to follow. Sure, it may avoid full-page crashes—but at a cost.

Here are two examples of the different approaches I mentioned. Either you have an ever-growing scope:

try {
  const userData = await api.getUserData();

  const organizationData = await api.getOrganizationData(userData.organizationId);

  return { userData, organizationData };
} catch (e) {
  // Handle multiple errors
}

Or where variables "escape" the scope of the try-catch:

let userData: UserData | null = null;
let organizationData: OrganizationData | null = null;
try {
  userData = await api.getUserData();
} catch (e) {
  // Handle/rethrow/rewrap error
}

if (!userData) {
  // Potential further error-throwing due to missing data
}

try {
  organizationData = await api.getOrganizationData(userData.organizationId);
} catch (e) {
  // Handle/rethrow/rewrap error
}

if (!organizationData) {
  // Potential further error-throwing due to missing data
}

return { userData, organizationData };

Languages like C# have tried to improve things with features like type-conditional catches, but ultimately, the same issues persist: the more logic you put in a try block, the more your catch turns into a cluttered mess of conditionals. And if you keep try scopes tight, you often find yourself battling linters or ending up with excessive optional chaining. At Pistachio, we're transitioning to an approach inspired by Go—and it’s been a game changer.

Go’s Simple, Elegant Approach

For readers who haven’t had the pleasure of writing Go yet—strap in. Go makes you think about errors in every function. Always. In theory, a Go program shouldn’t have runtime exceptions in the traditional sense. The pattern is simple: a function returns either a result and nil for the error, or nil for the result and a real error.

Here’s a basic, simplified example:

u, err := getUserData(userID)
if err != nil {
    return fmt.Errorf("error getting user in get user handler: %w", err)
}

err = sendUserNextSimulation(u)
if err != nil {
    return fmt.Errorf("error sending simulation to user in handler: %w", err)
}

return nil

This mutual exclusivity keeps things clean. If you receive an error, there is no result—and vice versa. You handle it immediately, right there, while the context is still fresh in your head. No jumping. No surprises. No GOTO.

To adopt this in JavaScript, we’ve made a few tweaks. Our pattern flips the position of the values to emphasize handling the error first. And in our refactored code, we don’t throw—ever.

So, let's refactor the two examples from earlier, both using try-catch in different ways:

const [errUser, userData] = await httpClient.get('/users/{userId}', { userId });
if (errUser) {
  return [new Error('error getting user-data in loader', { cause: errUser }), null];
}

const [errOrganization, organizationData] = await httpClient.get('/organizations/{organizationId}', {
  organizationId: userData.organizationId,
});
if (errOrganization) {
  return [new Error('error getting organization-data in loader', { cause: errOrganization }), null];
}

return [null, { userData, organizationData }];

The resulting code might have a higher line count, but ultimately we end up with code that’s clearer, more concise, and easier to debug. You get a direct 1:1 relationship between error and handling. You don’t need to worry about try-catch scopes or jump between lines to understand the full picture.

Wrapping and Extending

For third-party or legacy code that still throws, we use a wrapper that converts thrown exceptions into our preferred return-style format. That allows us to keep a consistent error-handling model across the board—without rewriting every package or SDK we interact with.

Here’s our simple wrapper that turns try-catchable promises into [error, result] tuples:

export type SafeWrap<ErrorType = Error, DataType = unknown> =
  | [error: ErrorType, data: null]
  | [error: null, data: DataType];

export async function safeWrap<ErrorType = Error, DataType = unknown>(
  promise: () => Promise<DataType>,
): SafeWrap<ErrorType, DataType> {
  try {
    const data = await promise();
    return [null, data];
  } catch (error) {
    return [error as ErrorType, null];
  }
}

As demonstrated, try-catch is still a "necessary evil"—but we can abstract it away, so that any consumer (internal or external) stays clear and readable:

const [err, file] = await safeWrap(() => storageClient.get('/file-reference'));
if (err) {
  return [new Error('error getting file-reference from storageClient', { cause: err }), null];
}

return [null, file];

The result? Safer, clearer, and more debuggable code. We’re not skipping error handling anymore—we’re required to confront it as it arrives. Of course, try-catch still exists in our codebase. There’s no way to fully eliminate it, especially when integrating with code we don’t control. But we’ve been able to abstract it away, reduce its surface area, and let developers write cleaner, more focused logic.

Objectively? This is just a better way to handle errors. Cleaner, more readable, and without the ancient baggage of jumping from one part of your logic to another. It’s time for try-catch—the modern GOTO—to finally GOAWAY.

Even if this is a somewhat optimistic interpretation of Dijkstra’s2 famous words, I like to think he'd agree:

“The quality of programmers is determined by the quality of their control structures.”

You made it this far, so maybe we’ve won one more developer over to a better way of handling errors in JavaScript—and maybe even in programming as a whole.

  1. GOTO statements are a programmatic way to perform a one-way jump to either a specified named label or line of code, skipping anything in between.↩

  2. Edsger W. Dijkstra, “Go To Statement Considered Harmful” (1968)↩

Share article
Share article
Share article

We believe in a learn-by-doing approach over snooze-worthy videos.

Instead, Pistachio tailors real-world scenarios so your team can build their cyber skills in a safe environment.

Start Free Trial
Start Free Trial
Pistachio activity card showing simulation results.Pistachio activity card showing simulation results.
Book a demo

Ready to reduce your human risk? Book your demo today.

Fernando Croca
IT & Services Director
Plateform
Role,Company

“We lacked confidence that employees would recognise today’s phishing threats or respond correctly. Since using the Pistachio platform, they are more attentive and proactive in identifying potential risks. The solution is easy to manage, quick to implement, and highly effective.

Johan Haugland
Sikkerhetsansvarlig (CISO)
Horde AS
Role,Company

“The platform is easy to implement and provides clear insights into employee phishing awareness. Its bite-sized learning encourages participation, saves administration time, and allows us to focus more on real threat detection.”

Ana Catarina Silva
Privacy and Cybersecurity and Data Protection Officer
Antas da Cunha Ecija
Role,Company

“We were looking for a practical way to improve cybersecurity awareness across the organization. Pistachio made training simple and engaging, and we’ve seen a noticeable improvement in employee awareness with minimal effort.”

Karsten Winther Hansen
Operations Director
Frie
Role,Company

“Pistachio removed the burden of planning campaigns, collecting data, and evaluating results. The program challenges each employee at their own level and provides clear, simple reporting. Pistachio has streamlined our awareness training and strengthened our organization’s cybersecurity.”

Matti Hakkarainen
ICT Manager
I-Valo Oy
Role,Company

“Our goal was to help employees better recognize phishing emails and avoid malicious links. Pistachio made this easy and engaging, and we’ve seen a clear improvement in detection, fewer clicks on phishing links, and reduced data disclosure. With minimal ongoing maintenance, it’s an effective and low-effort solution.”

Joseph Cunningham
Head of IT Security & Support
OOONO
Role,Company

“Within weeks, Presence had flagged several incidents that could have otherwise been missed, including employees forwarding company data to personal accounts during offboarding. The alerts are reliable and actionable, so we've built a full investigation process around them. Although I was skeptical at first, the value was obvious almost immediately.”

Anthony Balfour
IT Security Specialist
Lhoist Group
Role,Company

“Pistachio has reduced our manual workload to zero with its automated phishing simulations and in‑box delivery of scenario‑based trainings. This has allowed our team to dedicate time to other important security initiatives while Pistachio handles our Security Awareness program seamlessly.”

Carrie Sheen
IT Operations Director
Auxo Talent
Role,Company

“Presence automatically detects suspicious behaviour and gives us the visibility we need without constant manual oversight. Within the first few weeks we were already seeing alerts that would have otherwise gone unnoticed as the system learns what is usual and unusual behaviour for our environment.”

Leon Flikkenschild
CCO
IT Synergy
Role,Company

“Before Pistachio, security awareness was something we could only offer to a few customers. Today it’s part of every managed service we deliver, and we haven’t had to grow the team to make that happen.”

Fill out the form to book a demo:

Trusted by 1200+ customers & MSPs