A developer's year-long journey from Rust to TypeScript reveals how the absence of Result types creates fragile codebases, and how libraries like neverthrow attempt to bridge the gap between exception-driven and type-safe error handling.
Programming languages shape how we think about failure. This is not merely an aesthetic preference or a matter of syntactic sugar. The way a language handles errors fundamentally influences the architecture that emerges from it, the assumptions developers make about control flow, and the confidence they can have in their systems at runtime. A new blog post from Code Input captures this tension beautifully, documenting one developer's transition from Rust to TypeScript and the specific friction that arises when you have internalized a different philosophy of how programs should fail.
The author's core complaint is precise and technical: TypeScript lacks the ergonomics of Rust's Result type, and this absence makes error handling in TypeScript codebases significantly more difficult to reason about. In Rust, functions declare their failure modes explicitly. When you write fn calc_operation(var_1: Type1, var_2: Type2) -> Result<i32>, you are making a contract visible at the type level. Anyone calling this function knows it might fail, and more importantly, they must handle that possibility. The question mark operator (?) provides elegant syntax for propagating errors upward while keeping the happy path readable.

The author provides a concrete example that illustrates the cumulative benefit of this approach. Looking at a Rust function, they can immediately discern that intermediate values are unwrapped integers, that sub-operations return custom Result types, and that the function is unlikely to panic in unexpected ways. Failure is structured, traceable, and explicit. The type system enforces a discipline that exception-based languages leave to convention.
TypeScript, by contrast, inherits JavaScript's exception-driven error model. Functions can throw anything at any time, and there is no type-level mechanism to declare which errors a function might produce. You can document errors in JSDoc comments, but documentation and enforcement are different things. The author notes that TypeScript typing is "genuinely hard" and that many codebases end up polluted with any and unknown types, which undermines the entire purpose of static typing. When errors are untyped, they become opaque. When they are opaque, they become difficult to handle systematically.
The solution the author settles on is neverthrow, a TypeScript library that provides Result and Option types modeled after Rust's approach. The library is not a direct port. TypeScript has different constraints, different type system capabilities, and a different runtime model. But it captures enough of the essential pattern to make a meaningful difference in code quality.
What makes neverthrow valuable is not just the Result type itself, but the ecosystem of patterns it enables. The author describes creating custom error types that serve as a centralized contract for the entire application. When every function returns Result<T, APXError>, error handling becomes consistent and predictable. You can report errors to external services with uniform formatting. You can display user-facing messages with consistent styling. You can trace failures through the call stack with structured metadata rather than parsing stack traces.
The technical implementation reveals the friction involved in adapting Rust patterns to TypeScript. The author must re-export and wrap several neverthrow types to create a cohesive custom error system. This is more boilerplate than Rust requires, where custom error types integrate naturally with the standard library. But the author notes that LLMs can help generate this boilerplate, suggesting that the cost is one-time rather than ongoing.
More challenging is the absence of a question mark operator equivalent. In Rust, you can chain operations that return Result types seamlessly. The ? operator handles unwrapping successful values and propagating errors. TypeScript has no syntactic equivalent, so the author turns to generator functions and neverthrow's safeTry wrapper. The yield keyword enables early exit from the generator when an error occurs, mimicking the short-circuit behavior of Rust's operator.
The resulting code is functional but not elegant. Generator functions add cognitive overhead. The yield* syntax is unfamiliar to many TypeScript developers. The pattern works, but it feels like a workaround rather than a natural language feature. This is the fundamental limitation of porting concepts between languages with different paradigms. You can approximate the behavior, but you cannot replicate the integration with the language's core syntax.
The author briefly considers Effect, a more comprehensive TypeScript library that provides typed errors, recovery APIs, tracing, and other features. Effect represents a more ambitious attempt to bring functional programming patterns to TypeScript. But the author expresses skepticism about frameworks that bundle extensive functionality, noting that there is usually a cost somewhere. Effect's runtime evaluation model and steep learning curve make it a harder sell for developers who want targeted improvements rather than wholesale architectural changes.
This skepticism reflects a broader tension in the TypeScript ecosystem. The language occupies an awkward middle ground between the expressiveness of Haskell or Rust and the pragmatism of JavaScript. Libraries like neverthrow and Effect attempt to pull TypeScript toward more rigorous functional patterns, but they fight against the grain of the language's design. TypeScript was built to be a gradual typing layer over JavaScript, not a pure functional language. The compromises that make it accessible to JavaScript developers also limit how far you can push type-level programming.
The implications of this analysis extend beyond the specific libraries discussed. The post raises fundamental questions about how programming languages should handle failure. Should errors be exceptional, reserved for truly unexpected conditions? Or should they be ordinary values that functions return, integrated into the type system and handled explicitly? Rust's approach treats errors as first-class citizens of the type system. JavaScript's approach treats them as control flow interruptions that bypass normal return paths.
Neither approach is objectively correct. Exception-based error handling can produce cleaner code in simple cases, where you want to handle errors at a high level without explicitly threading Result types through every intermediate function. But it also makes it easy to ignore errors entirely, to let exceptions bubble up unhandled until they crash the application. Result-based error handling requires more explicit code, but it also makes the failure modes of every function visible and enforceable.
The author's year-long experience suggests that the Result approach scales better in large codebases with many developers. When error handling is left to convention, conventions diverge. When it is enforced by the type system, consistency emerges naturally. The question is whether the friction of approximating Rust patterns in TypeScript is worth the benefit. For the author, the answer is yes. neverthrow provides enough of the ergonomic benefit to justify the added complexity.
What remains unclear is how these patterns interact with TypeScript's broader ecosystem. Libraries, frameworks, and third-party APIs generally expect exception-based error handling. Using Result types creates an impedance mismatch at system boundaries. You must wrap external calls in Result-returning functions, translating between error handling paradigms. This wrapping adds code and potential failure points. The author does not address this challenge, but it is a significant consideration for anyone adopting these patterns in production.
The post ultimately serves as a meditation on what we lose when we switch programming languages. Syntax can be learned quickly. APIs can be memorized. But the deeper patterns, the assumptions about how code should be structured and how systems should fail, take longer to internalize. The author's year-long journey from Rust to TypeScript is a reminder that language choice is not just about capability. It is about philosophy, and the friction that arises when philosophies collide.

Comments
Please log in or register to join the discussion