Close

Via Don Minzoni, 59 - 73025 - Martano (LE)

Error Handling in TypeScript
Web development

How to Improve Error Handling in TypeScript (Without Losing Your Mind)

By, Claudio Poidomani
  • 12 Jan, 2026
  • 69 Views
  • 0 Comment

In web development, it’s easy to write code that works, and then immediately move on to the next feature. But what happens when that code fails?

Recently at Fyonda, we ran into exactly this situation. While reading existing code and reviewing pull requests, we noticed a recurring issue: error handling in TypeScript is often unclear and hard to reason about. From this experience, two approaches emerged. We tested them, refined them, and now we want to share them. If you write JavaScript or TypeScript, this article is for you.

_The problem: “It didn’t throw any errors, so I assumed it worked”

Imagine you need to use a function that returns a Promise<boolean>. At first glance, everything seems clear. But what happens when something goes wrong?

In the JS/TS world, there are two critical situations that the return type doesn’t tell you about:

  • There is an explicit throw inside the function, but the only way to discover it is by reading the implementation.
  • The function can fail without an explicit throw because it relies on APIs that may throw internally (for example JSON.parse). In this case too, you either need to know it in advance or discover it at runtime.

The point is that whoever calls the function doesn’t know if or when an error might occur, nor where it might come from. The only way to find out is to open the file and read the code. And that’s a problem.

This uncertainty creates friction during pull requests, leads to hard-to-debug production issues, and encourages wrong assumptions.It’s not enough for code to “work”: it should make how and when it can fail explicit.

_Solution #1: a simple try-catch wrapper

As a first step, we adopted a very simple wrapper that returns a tuple [error, data].

It’s lightweight, explicit, and introduces no new dependencies.

Example:

typeResult<T> = [Error |null, T |null];

asyncfunction safe<T>(fn:() =>Promise<T>):Promise<Result<T>> {
try {
const data =awaitfn();
return [null, data];
  }catch (err) {
return [errasError,null];
  }
}

// Usage:
const [err, value] =awaitsafe(() =>syncStore());
if (err) {
console.error("Error during sync:", err);
return;
}

Pros

  • Callers immediately know they must handle errors.
  • The structure is familiar (similar to useState or common Python patterns).
  • The control flow becomes explicit.

Cons

  • The error is not typed: it’s just a generic Error.
  • You can still forget to handle it properly.
  • It doesn’t scale well for complex projects.

For internal scripts, quick tools, or small functions, this approach is often enough. But for core modules, we wanted something stronger.

_Solution #2: type failures with neverthrow

For larger projects, we explored the neverthrow library, which brings a Rust-like pattern to TypeScript: returning a Result type instead of throwing.

Example:

import { ok, err,Result }from"neverthrow";

typeSyncError =
  | {kind:"ShopifyError";message:string }
  | {kind:"DbError";message:string };

asyncfunctionsyncStore():Promise<Result<boolean,SyncError>> {
try {
awaitcallShopify();
awaitsaveToDb();
returnok(true);
  }catch (e:any) {
if (e.response?.status ===404) {
returnerr({kind:"ShopifyError",message:"Store not found" });
    }
returnerr({kind:"DbError",message: e.message });
  }
}

Usage:

const result =awaitsyncStore();

if (result.isErr()) {
switch (result.error.kind) {
case"ShopifyError":
// specific handling
break;
case"DbError":
// different handling
break;
  }
return;
}

// All good:
console.log("Sync completed:", result.value);

Pros

  • Full type safety: you always know which errors you need to handle.
  • No more scattered try-catch blocks.
  • Improved readability and maintainability.

Cons

  • Requires more code.
  • Can feel intimidating at first (but it’s simpler than it looks).
  • Works best when adopted consistently across the team.

At Fyonda, we’re introducing this approach in key modules, and the impact on code quality is already clear.

_When should you use one or the other?

ScenarioRecommended solution
Internal scripts, quick toolstry-catch wrapper
APIs, shared modules, librariesneverthrow (or similar)
Growing projectsStructure error handling early
Critical refactors or reviewsExplicitly typed errors

_Conclusion

Error handling isn’t a minor technical detail , it’s a design choice.

It’s about the people reading your code, the ones who will work on it six months from now, and the ones using it in production without knowing what to expect when something goes wrong.

Too often, we write code that “works” but doesn’t clearly communicate what happens on failure. That’s where hard-to-trace bugs, PR debates, and last-minute patches come from. That’s why we chose to make errors explicit: declaring them in return types, using tools that force clarity, and designing code that is more structured and reliable. It’s not about avoiding errors.

It’s about accepting them as part of the flow, and designing your code so it can handle them, communicate them clearly, and lead you to cleaner solutions.