TypeScript中如何实现规范的错误处理?基于neverthrow与数据库查询场景的实践疑问
Great question—this is a super common point of confusion when shifting from traditional try/catch to result-based error handling libraries like neverthrow. Let me break this down for you:
First: Yes, neverthrow is absolutely worth using
The core value of neverthrow isn't to eliminate try/catch entirely—it's to enforce explicit, consistent error handling across your codebase. Here's why your current approach is still better than raw try/catch:
- You can't accidentally forget to handle errors: With raw try/catch, it's easy to skip a catch block and let errors bubble up unexpectedly. neverthrow forces you to handle both
OkandErrbranches (either via.match(),.isOk()/.isErr(), or unwrapping with.unwrap()which makes intentional failures explicit). - Error types are self-documenting: Your return type clearly tells callers exactly what errors they might encounter (
DATA_NOT_FOUNDorDATABASE_ERROR), which makes your code easier to reason about and maintain. - No hidden control flow: Try/catch introduces implicit control flow that can make code harder to follow. neverthrow keeps all success/failure paths visible in the function's return type.
Why you still need try/catch here
Database operations (like your db.select() call) are inherently "throwable"—they can emit unexpected errors (connection timeouts, driver bugs, network issues) that aren't part of the expected business logic. Try/catch is the only way to capture these low-level, runtime exceptions so you can convert them into a structured Err value that fits your neverthrow workflow.
This is a standard pattern with result-based error handling: you use try/catch at the "boundary" where external code (like a database driver) might throw, then convert those throws into Err values. This way, all code above that boundary can use neverthrow's consistent API instead of dealing with scattered try/catch blocks.
Optimizing your current implementation
Your code is already on the right track, but here are a few tweaks to make it cleaner and more scalable:
1. Extract database error handling into a reusable utility
Instead of repeating try/catch in every database function, create a wrapper that converts throwable database queries into Result values:
// src/utils/db.ts async function runDbQuery<T>(query: Promise<T[]>): Promise<Result<T[], { type: "DATABASE_ERROR"; error: string }>> { try { const data = await query; return ok(data); } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unexpected database failure"; return err({ type: "DATABASE_ERROR", error: errorMessage }); } }
2. Simplify your fetch function with the wrapper
Now your fetchUserProfiles can focus on business logic instead of low-level error handling:
export const fetchUserProfiles = async ( id: string, ): Promise<Result<userProfileType, { type: "DATA_NOT_FOUND" | "DATABASE_ERROR"; error: string }>> => { // Delegate database error handling to our wrapper const queryResult = await runDbQuery( db.select().from(schema.userProfiles).where(eq(schema.userProfiles.userId, id)) ); // Propagate database errors directly if (queryResult.isErr()) { return queryResult; } // Handle business logic (empty result = not found) const [profile] = queryResult.value; if (!profile) { return err({ type: "DATA_NOT_FOUND", error: "User profile doesn't exist", }); } return ok(profile as userProfileType); };
3. Use the result consistently in callers
The real payoff comes when you use this function elsewhere—you get clear, type-safe error handling without try/catch:
async function handleUserProfileRequest(userId: string) { const profileResult = await fetchUserProfiles(userId); return profileResult.match( (profile) => ({ status: 200, data: profile }), (error) => { if (error.type === "DATA_NOT_FOUND") { return ({ status: 404, message: error.error }); } else { // Log the database error for debugging console.error("Database error fetching profile:", error.error); return ({ status: 500, message: "Failed to load user profile" }); } } ); }
Final takeaway
neverthrow doesn't replace try/catch—it complements it. You still need try/catch at the boundaries where external code throws, but converting those throws into Result values lets you maintain a consistent, explicit error handling pattern throughout the rest of your application. This makes your code more robust, easier to debug, and less prone to unhandled error crashes.
内容的提问来源于stack exchange,提问作者Roso

