In TypeScript, we’ve been told that try/catch is the standard for error handling. But there’s a catch: native exceptions are essentially invisible GOTO statements that jump over layers of your application, making it incredibly hard to reason about state.
Even worse, because JavaScript allows you to throw anything (from strings to dates), TypeScript is forced to treat every caught error as unknown. This creates a type-erasure effect where your function's failure states become invisible to the compiler and uncontracted in your signatures.
I built ripthrow to fix this. It’s a 1.6KB, zero-dependency library that brings Rust-inspired error handling to TypeScript with a focus on pragmatism and raw performance.
22x Performance over native throw
Most people don't realize how expensive throw actually is. Based on my benchmarks (running on Bun 1.3), the difference is staggering:
| Pattern | Operations/sec | Latency | vs Native |
|---|---|---|---|
| Err() (ripthrow) | 25,357,831 | 41.6 ns | — |
| throw (Native) | 1,182,517 | 1047.0 ns | 22x slower |
By returning a Result instead of throwing, you get the same speed as a manual object literal because ripthrow uses POJOs (Plain Old JavaScript Objects) instead of expensive class allocations. In real-world scenarios, the overhead is a negligible 15-20 ns per operation.
How it looks in practice
Stop nesting if statements and losing your types. With the AsyncResultBuilder, you can create fluent pipelines that are 100% type-safe:
const Errors = createErrors({
UserNotFound: { message: (id: number) => `User #${id} not found` },
});
function getUserName(userId: number) {
// Wrap promises safely without try/catch blocks
// Note: ripthrow offers multiple wrapping strategies; see the GitHub Wiki for details.
return AsyncResultBuilder.safeAsync(db.users.findById(userId))
.mapErr(() => Errors.UserNotFound(userId))
.map((user) => user.name)
}
const userName = await getUserName(123);
if (userName.ok) {
console.log(`Hi! ${userName.value}`); // Fully typed success
} else {
console.log(userName.error.message); // "User #123 not found"
}
Exhaustive Matching
One of the biggest risks of manual error handling is forgetting a case. While other libraries require manual switch statements or never checks, ripthrow has a built-in fluent API for this.
If you add a new error type to your application and forget to handle it, your code won't compile.
// This will throw a type error at compile-time if 'NetworkError' is not handled
// Note: defining explicit `data: User` type is required to ensure that you're handling all the errors
const data: User = matchErr(result)
.on(Errors.UserNotFound, (err) => ...)
.exhaustive();
Why ripthrow?
While other libraries exist, ripthrow is designed as a "missing operator" for modern ESM:
- Minimalist: Only ~1.6 KB (min+gzip).
- No Classes: Uses simple
{ ok: true, value }structures for maximum speed. - Exhaustive Matching: Use
.exhaustive()to ensure at compile-time that you’ve handled every possible error variant. - Collision-Free: Unique Symbols identify error types, preventing collisions between different packages.
If you want to make your TypeScript applications more resilient and faster, ripthrow is now at v3.0 - Stable.
Check it out on GitHub: MechanicalLabs/ripthrow

Top comments (0)