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<number, never>
-> If successful, it holds a number. -
Err<never, "Invalid user ID">
-> 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.
Top comments (4)
Thx for this post, I never try to give type for a throw based solution (maybe I never used). This is show TypeScript is not finished yet, because don't have option to handle throw error situation which sometimes is important.
I also missing from typescript a function purity check.
Thrown errors are not typed because the return value is never returned. Throwing something takes an entirely different path that has nothing to do with typing the return value.
Yes you right, I know how works of thrown. I know that is not return type, instead that is will be a thrown type, because that is descrebe more precise the behaviour of function.
Out of the 3 reasons in the Why is this a problem? section, only the third is valid. The first one is not because throwing doesn't affect the return type of the function. When something is thrown from a function, there is no return value. This is also not an unexpected crash. After all you did write the code with the
throw
statement in it, right? How can it be unexpected?The more accurate reasons to pick branching over throwing are:
Overall, yes, branching is better than throwing. The golden rule is: You only throw if you cannot write code for the case. This is almost exclusively contained in the realm of libraries, where the library must cater for the majority of its users, for the vast majority of the use cases. Application code can most likely be free of thrown errors because we usually know what to do when a user ID is not found. At the very minimum, we can exchange one piece of UI for another.