DEV Community

Saurabh Kumar
Saurabh Kumar

Posted on

I got tired of try/catch lying to me. So I built result.js.

You call JSON.parse. It looks safe. Nothing in the signature tells you otherwise.

Then someone passes garbage in production and your server crashes.

That's not a bug. That's try/catch being invisible.


The problem

function parseConfig(text: string): Config {
  return JSON.parse(text); // throws SyntaxError. you'd never know.
}
Enter fullscreen mode Exit fullscreen mode

This function lies. It says it returns a Config. It doesn't say it might explode. TypeScript doesn't warn you. Your tests probably don't catch it. And the person calling this function has no idea they need to wrap it.

The failure is real. It's just hidden.


What if failure was part of the type?

function parseConfig(text: string): Result<Config, SyntaxError> {
  return wrap(() => JSON.parse(text));
}
Enter fullscreen mode Exit fullscreen mode

Now the signature is honest. Two possible outcomes. Both visible. TypeScript knows about both. The person calling this function knows about both.

That's result.js.


The API

Two constructors:

const success = ok({ id: 1, name: "Alice" });
const failure = err({ code: 404, message: "Not found" });
Enter fullscreen mode Exit fullscreen mode

Handle both outcomes with match — both arms required, TypeScript won't compile if you skip one:

result.match({
  ok:  user  => `Welcome, ${user.name}`,
  err: error => `Failed: ${error.message}`,
});
Enter fullscreen mode Exit fullscreen mode

Chain fallible operations without nesting with flatMap:

const dashboard = getUser(id)
  .flatMap(user  => getPermissions(user))
  .flatMap(perms => buildDashboard(perms))
  .unwrapOr(emptyDashboard);
Enter fullscreen mode Exit fullscreen mode

If any step fails, the rest are skipped. The error falls through to unwrapOr. No if (result.isErr()) checks stacking up.

Adopt existing throwing code at the boundary:

const parsed = wrap(
  () => JSON.parse(text) as Config,
  e  => ({ code: "PARSE_ERROR", detail: String(e) })
);
Enter fullscreen mode Exit fullscreen mode

Async works too

ResultAsync wraps a Promise<Result<T, E>> and gives you the same chaining API. Write the whole pipeline, await once at the end.

const name = await ResultAsync
  .from(fetchUser(id), e => new ApiError(e))
  .flatMap(user => ResultAsync.from(fetchProfile(user.profileId), e => new ApiError(e)))
  .map(profile => profile.displayName)
  .unwrapOr("Anonymous");
Enter fullscreen mode Exit fullscreen mode

No nested awaits. No growing stack of error checks. One await at the end.


When to use it

Use Result when the failure is an expected outcome — parsing input, querying a database, calling an API.

Stick with try/catch when the failure is a bug — out of memory, programmer errors, things that should never happen.

You don't have to pick one everywhere. wrap converts throwing code at the boundary so you can work with Result from there.


Install

npm install @bugfreedev/result.js
Enter fullscreen mode Exit fullscreen mode

Zero dependencies. Full TypeScript. Ships with type declarations — no @types package needed.

import { ok, err, wrap, ResultAsync } from "@bugfreedev/result.js";
import type { Result } from "@bugfreedev/result.js";
Enter fullscreen mode Exit fullscreen mode

The idea is simple: functions that can fail should say so. Not in a comment. Not in the docs. In the type signature, where TypeScript can see it and so can anyone reading the code.

That's the whole thing.

Links

If you try it, I'd love feedback on the API design, naming, and overall developer experience. The goal is to keep it small, obvious, and pleasant to use without turning it into a mini framework.

Top comments (1)

Collapse
 
bug-free-dev profile image
Saurabh Kumar • Edited

Thanks for reading.

This library is still early, so now is the perfect time to challenge the API before it becomes harder to change.

If you have experience with Result types (Rust, neverthrow, Effect, fp-ts, etc.), I'd love to hear:

  • What feels awkward or unnecessary?
  • Which names would you change?
  • Should ResultAsync exist?
  • Is the API simple enough for developers who have never used Result types before?

GitHub: github.com/bug-free-dev/result.js

npm: npmjs.com/package/@bugfreedev/resu...

Brutally honest feedback is welcome. The goal isn't to build the most powerful Result library. It's to build one that's easy to understand, easy to teach, and pleasant to use.