You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

TypeScript中如何实现规范的错误处理?基于neverthrow与数据库查询场景的实践疑问

Is neverthrow still useful if I still need try/catch for database errors?

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 Ok and Err branches (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_FOUND or DATABASE_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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.28 06:37:52