The most important function in my codebase
Based on Theo - t3․gg's video on YouTube. If you like this content, support the original creators by watching, liking and subscribing to their content.
Stop treating “return type” as proof of success in TypeScript when functions can throw; thrown errors are invisible to the type system.
Briefing
TypeScript’s biggest practical weakness isn’t missing features—it’s that thrown errors and untyped failure paths let “number” look safe even when the function sometimes fails. The core fix proposed here is to stop relying on try/catch for control flow and instead make failure explicit in the type system, so callers can’t accidentally treat an errored result as valid data.
The frustration starts with a common pattern: a function that may throw still appears to return a plain value type. In the example, an async math function sometimes throws based on Math.random. TypeScript happily infers that the awaited result is a number, so downstream code proceeds as if the value always exists. When the function throws, the error bubbles to the top scope, and the whole program path can collapse—often without any local handling that would let the code recover or produce a useful message.
A naive try/catch approach “works” but quickly becomes brittle. Catch blocks force extra indentation, and when multiple async calls are chained, errors get nested and rethrown in ways that obscure where the failure happened. Even worse, rethrowing can cause the outer layer to lose context, leaving developers with generic failures and limited debugging leverage.
The proposed remedy is a small wrapper—about 25 lines—that converts thrown exceptions into a structured return value. Instead of “throw on failure,” the wrapper returns a tuple-like result: data is either the expected value or null, and error is either null or an Error object. By moving the logic into a helper function, the caller can safely branch on error before using data, and TypeScript can narrow types accordingly. The transcript emphasizes that this is a bare-minimum baseline: it doesn’t eliminate try/catch everywhere, but it prevents the most dangerous failure mode—treating “maybe failed” work as “definitely succeeded.”
From there, the discussion expands into a spectrum of error-handling strategies. “Never throw” pushes the idea further: instead of throwing, functions return result types (success vs failure) at every layer. That enables exhaustive error handling—down to specific error variants—so the system can send the right user-facing message depending on whether authentication failed, a token check failed, a rate limit hit, or a model was offline. The tradeoff is friction: async composition becomes more awkward, and developers may need to unwrap results repeatedly.
At the top of the spectrum sits Effect, described as a deeper shift in how programs are built—less “TypeScript with better types” and more a different language-like runtime model. Effect can represent failure as part of the computation itself (e.g., effect.fail vs effect.succeed), enabling safer composition without relying on exceptions. The cost is buy-in: adopting Effect requires rewiring mental models and refactoring toward functional chaining patterns.
The takeaway is pragmatic. For most teams, start by making failure explicit with a wrapper like the one shown. Then graduate to result-based approaches like never throw when layering complexity grows. Effect becomes relevant when the goal is to make errors rare by construction, at the expense of a larger migration. The message ends with a call to stop “throwing and hoping,” because as systems scale, hidden failure paths turn into reliability and debugging debt.
Cornell Notes
TypeScript often treats a function’s return type as guaranteed, even when the function can throw at runtime. That mismatch leads to code that assumes “data exists” while failures bubble up unpredictably. A practical fix is a small try/catch wrapper that converts exceptions into an explicit result shape: either data (or null) plus an error (or null). With that structure, callers can branch on error first, letting TypeScript narrow types safely. For more complex, multi-layer systems, the transcript argues for result-based patterns like never throw, and for the most rigorous approach, Effect—though it demands a bigger mental-model shift and refactor effort.
Why does TypeScript mislead developers about safety when async functions can throw?
What goes wrong with “just use try/catch” in nested or multi-call async code?
How does the 25-line wrapper improve type safety and control flow?
What does “never throw” add beyond the wrapper approach?
Why does Effect represent a bigger shift than never throw?
Where does the transcript place the “spectrum” of error handling?
Review Questions
- In the math example, what specific TypeScript inference leads to unsafe downstream code, and how does the wrapper change that outcome?
- Compare the wrapper approach and never throw: what new capability appears when errors are returned as typed variants at every layer?
- What tradeoffs does the transcript associate with Effect adoption compared with never throw, especially regarding readability and team buy-in?
Key Points
- 1
Stop treating “return type” as proof of success in TypeScript when functions can throw; thrown errors are invisible to the type system.
- 2
A small try/catch wrapper can convert exceptions into an explicit {data, error} result shape, enabling safe branching and type narrowing.
- 3
Avoid nested try/catch and rethrow patterns in multi-await code paths because they obscure where failures originate and complicate recovery.
- 4
For layered systems with many failure modes, prefer result-based approaches (like never throw) so callers can do exhaustive checks and return precise user-facing messages.
- 5
Use a spectrum mindset: start with the wrapper baseline, move to never throw as complexity grows, and consider Effect when you want errors handled by construction.
- 6
Recognize the migration cost: never throw adds unwrapping/return-type friction, while Effect requires a deeper mental-model shift and refactor effort.
- 7
Don’t rely on “throw and hope” as software scales; hidden failure paths become reliability and debugging debt.