Handling Errors in TypeScript: Stop Throwing, Start Returning
In TypeScript, we often assume that functions will always return their expected values. But what happens when something goes wrong? Many functions can throw errors, yet TypeScript still types them as if they always succeed. This can lead to unexpected crashes and misleading type information. Let’s break this down with an example and see how neverthrow fixes it—without even needing explicit types! The problem: Hidden Exceptions Let’s say we have a function that fetches a user’s age from a database. If the user ID is invalid, it throws an error. function getUserAge(userId: number) { if (userId | Ok Ok -> If successful, it holds a number. Err -> If it fails, it holds the exact error string. Why is this better? No more hidden exceptions: We never throw errors, so our program never crashes unexpectedly. More precise error types: Instead of using generic string or Error, TypeScript automatically infers the exact error values. Explicit, safe handling: Since Result forces us to handle both cases, we write clearer and safer code: if (ageResult.isErr()) { console.error("Error:", ageResult.error); // Typed as "Invalid user ID" } else { console.log("User age:", ageResult.value); // Typed as number } Even better: functional handling Instead of using if statements, we can chain the result with .map() and .mapErr(): ageResult .map(age => console.log("User age:", age)) // Runs if success .mapErr(error => console.error("Error:", error)); // Runs if failure This makes our code cleaner and easier to read! Final takeaway Using neverthrow eliminates hidden exceptions and makes code more predictable. Instead of throwing errors, we return them explicitly, ensuring TypeScript correctly infers both success and failure cases. This approach prevents misleading types and unexpected crashes while forcing proper error handling. TypeScript naturally infers precise types, leading to cleaner, safer, and more reliable applications.

In TypeScript, we often assume that functions will always return their expected values. But what happens when something goes wrong? Many functions can throw errors, yet TypeScript still types them as if they always succeed. This can lead to unexpected crashes and misleading type information.
Let’s break this down with an example and see how neverthrow
fixes it—without even needing explicit types!
The problem: Hidden Exceptions
Let’s say we have a function that fetches a user’s age from a database. If the user ID is invalid, it throws an error.
function getUserAge(userId: number) {
if (userId < 0) throw new Error("Invalid user ID");
return 25; // Fake age for demo
}
const age = getUserAge(-1); // This might throw an error, yet it's typed as number
console.log("User age:", age);
Why is this a problem?
-
Misleading type: TypeScript infers
getUserAge()
as returning number, even though it can throw an error. - Unexpected crashes: If an invalid user ID is passed, the function never actually returns, and throws instead!
- No type safety for errors: There’s nothing in the function signature that warns us an error might occur.
When you hover over getUserAge
, TypeScript incorrectly shows:
function getRandomNumber(): number
The solution: Using neverthrow
Instead of throwing errors, let's use neverthrow
to return them explicitly:
import { ok, err } from "neverthrow";
function getUserAge(userId: number) {
if (userId < 0) return err("Invalid user ID"); // Explicit error
return ok(25); // Success case
}
const ageResult = getUserAge(-1);
What TypeScript infers automatically:
function getRandomNumber(): Err<never, "Invalid user ID"> | Ok<number, never>
-
Ok
-> If successful, it holds a number. -
Err
-> If it fails, it holds the exact error string.
Why is this better?
- No more hidden exceptions: We never throw errors, so our program never crashes unexpectedly.
-
More precise error types: Instead of using generic
string
orError
, TypeScript automatically infers the exact error values. - Explicit, safe handling: Since Result forces us to handle both cases, we write clearer and safer code:
if (ageResult.isErr()) {
console.error("Error:", ageResult.error); // Typed as "Invalid user ID"
} else {
console.log("User age:", ageResult.value); // Typed as number
}
Even better: functional handling
Instead of using if
statements, we can chain the result with .map()
and .mapErr()
:
ageResult
.map(age => console.log("User age:", age)) // Runs if success
.mapErr(error => console.error("Error:", error)); // Runs if failure
This makes our code cleaner and easier to read!
Final takeaway
Using neverthrow
eliminates hidden exceptions and makes code more predictable. Instead of throwing errors, we return them explicitly, ensuring TypeScript correctly infers both success and failure cases.
This approach prevents misleading types and unexpected crashes while forcing proper error handling. TypeScript naturally infers precise types, leading to cleaner, safer, and more reliable applications.