mcpField
E
error-boundaries

error-boundaries

github.com/yanko-belov开发1335 次安装40 周前更新
代码仓库

关于 error-boundaries

error-boundaries 是一项软件架构技能,说明如何在明确的边界捕获和处理错误,而不是在业务逻辑中随处分散捕获。它面向设计应用故障处理与组件降级策略的开发者。

主要功能

  1. 01识别架构中的错误边界
  2. 02指导捕获位置与错误传播
  3. 03说明边界处的优雅降级
  4. 04提供检查清单和边界示例

适用场景

  1. 01规划 HTTP 或命令行入口的错误处理
  2. 02隔离外部服务适配器中的故障
  3. 03为可选功能提供回退行为

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/yanko-belov/code-craft --skill error-boundaries

安装后在 Claude Code 中输入/error-boundaries确认已加载。

详细介绍

错误边界

概述

在逻辑边界处捕获错误,而不是在调用栈中的随机位置。

错误边界是经过策略性选择的捕获点,既能防止故障级联,又能实现优雅降级。应将它们放在架构边界处,而不是散布在业务逻辑中。

适用场景

  • 设计应用架构
  • 决定 try/catch 应该放在哪里
  • 防止一个故障导致所有功能崩溃
  • 实现优雅降级
  • 隔离各个组件

铁律

NEVER scatter try/catch randomly. Place catches at ARCHITECTURAL BOUNDARIES only.

没有例外:

  • 不是为了“防御性编程”
  • 不是为了“安全包装”
  • 不是为了“以防万一”
  • 不是因为“函数可能抛出异常”

边界是有意设置的。随机捕获会掩盖错误。

什么是边界?

边界是上下文发生变化的位置:

┌─────────────────────────────────────────────────────────────┐
│                         ENTRY BOUNDARIES                     │
│  HTTP Request → [BOUNDARY] → Application                    │
│  Message Queue → [BOUNDARY] → Handler                       │
│  CLI Command → [BOUNDARY] → Execution                       │
│  Cron Job → [BOUNDARY] → Task                               │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│                       INTERNAL BOUNDARIES                    │
│  Application → [BOUNDARY] → External API                    │
│  Business Logic → [BOUNDARY] → Database                     │
│  Core → [BOUNDARY] → Third-party Library                    │
│  Parent Component → [BOUNDARY] → Child Component            │
└─────────────────────────────────────────────────────────────┘

策略性地放置边界

入口边界:HTTP 控制器
// ✅ CORRECT: Top-level error middleware
app.use(errorMiddleware);

function errorMiddleware(
  error: Error,
  req: Request,
  res: Response,
  next: NextFunction
) {
  // This is THE boundary between HTTP and application
  
  if (error instanceof ValidationError) {
    return res.status(400).json({ error: error.message, fields: error.fields });
  }
  
  if (error instanceof NotFoundError) {
    return res.status(404).json({ error: error.message });
  }
  
  if (error instanceof UnauthorizedError) {
    return res.status(401).json({ error: 'Unauthorized' });
  }
  
  // Log unknown errors, return generic response
  logger.error('Unhandled error', { error, request: req.path });
  return res.status(500).json({ error: 'Internal server error' });
}

// ❌ WRONG: try/catch in every controller
async function getUser(req: Request, res: Response) {
  try {
    const user = await userService.findById(req.params.id);
    res.json(user);
  } catch (error) {
    // Scattered catch - duplicated across all controllers
    res.status(500).json({ error: 'Failed' });
  }
}

// ✅ CORRECT: Let errors propagate to middleware
async function getUser(req: Request, res: Response) {
  const user = await userService.findById(req.params.id);
  res.json(user);
  // Errors propagate to errorMiddleware
}
内部边界:外部服务适配器
// ✅ CORRECT: Boundary between your code and external service
class PaymentGatewayAdapter {
  async charge(amount: number, token: string): Promise<ChargeResult> {
    try {
      // External call - this is a boundary
      const response = await this.stripeClient.charges.create({
        amount,
        source: token,
      });
      return this.mapToChargeResult(response);
    } catch (error) {
      // Translate external error to domain error
      if (error instanceof Stripe.CardError) {
        throw new PaymentDeclinedError(error.message, error.code);
      }
      if (error instanceof Stripe.RateLimitError) {
        throw new PaymentServiceUnavailableError('Rate limited');
      }
      if (error instanceof Stripe.APIConnectionError) {
        throw new PaymentServiceUnavailableError('Connection failed');
      }
      throw new PaymentError('Unexpected payment error', { cause: error });
    }
  }
}

// ❌ WRONG: Let Stripe errors leak into business logic
// ❌ WRONG: Catch in OrderService instead of adapter
UI 边界:React 错误边界
// ✅ CORRECT: Component-level error isolation
class ErrorBoundary extends React.Component<Props, State> {
  state = { hasError: false, error: null };

  static getDerivedStateFromError(error: Error) {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    // Log to monitoring service
    errorService.report(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback || <DefaultErrorUI />;
    }
    return this.props.children;
  }
}

// Usage: Isolate features from each other
function App() {
  return (
    <Layout>
      <ErrorBoundary fallback={<DashboardError />}>
        <Dashboard />
      </ErrorBoundary>
      
      <ErrorBoundary fallback={<SidebarError />}>
        <Sidebar />
      </ErrorBoundary>
      
      {/* Sidebar error doesn't crash Dashboard */}
    </Layout>
  );
}

边界检查清单

添加 try/catch 前,请确认:

问题如果答案是否……
这里是上下文转换点吗?不要在这里捕获
捕获错误能阻止有意义的传播吗?不要在这里捕获
我能将错误转换为有意义的领域错误吗?不要在这里捕获
是否有具体的恢复操作?不要在这里捕获
这个边界会改变责任归属吗?不要在这里捕获

正确的错误传播模式

让错误在业务逻辑中继续传播:

// ✅ CORRECT: No random catches in service layer
class OrderService {
  async createOrder(data: OrderData): Promise<Order> {
    // Validate (may throw ValidationError)
    this.validator.validate(data);
    
    // Get user (may throw NotFoundError)
    const user = await this.userRepo.findById(data.userId);
    
    // Check business rules (may throw BusinessRuleError)
    this.rules.assertCanCreateOrder(user, data);
    
    // Process payment (may throw PaymentError from adapter)
    const payment = await this.paymentAdapter.charge(data.amount, user.paymentToken);
    
    // Create order (may throw DatabaseError from repo)
    const order = await this.orderRepo.create({
      ...data,
      paymentId: payment.id,
    });
    
    return order;
    // ALL errors propagate to controller boundary
  }
}

// ❌ WRONG: Defensive try/catch in service
class OrderService {
  async createOrder(data: OrderData): Promise<Order | null> {
    try {
      // ... same logic ...
      return order;
    } catch (error) {
      logger.error('Order creation failed', error);
      return null;  // Lost context, hidden failure
    }
  }
}

在边界处优雅降级

边界可以提供回退方案:

// ✅ CORRECT: Graceful degradation at recommendation boundary
class ProductPage {
  async load(productId: string) {
    // Core data - must succeed
    const product = await this.productService.getById(productId);
    
    // Recommendations - can fail gracefully
    let recommendations: Product[] = [];
    try {
      recommendations = await this.recommendationService.getFor(productId);
    } catch (error) {
      // Log but don't fail the page
      logger.warn('Recommendations unavailable', { productId, error });
      // Empty recommendations is acceptable fallback
    }
    
    return { product, recommendations };
  }
}

关键区别: 这是“必需”功能与“可选”功能之间的边界。 这不是随机的防御性编程,而是有意实现的优雅降级。

抵御压力的准则

1. “为了安全,把所有内容都包在 try/catch 中”

压力:“要有防御性,捕获所有错误”

回应: 分散的捕获会掩盖错误,并妨碍在边界处进行恰当处理。错误会传播是有原因的。

行动: 移除内部的捕获,只在边界处处理。

2. “函数可能抛出异常”

压力:“我应该捕获一下,以防万一”

回应: 这正是边界的用途。业务逻辑不应该关心错误处理。

行动: 让它抛出异常,由边界捕获。

3. “我想为错误添加上下文”

压力:“捕获、补充信息,再重新抛出”

回应: 只有在确实能补充有用上下文时才这样做。多数捕获后重新抛出只会徒增噪音。

行动: 只有在不包装错误就会丢失上下文时才包装。通常不会丢失。

4. “每个组件都应该处理自己的错误”

压力:“封装意味着就地处理”

回应: 组件应该抛出恰当的错误。捕获错误是边界的职责。

行动: 组件抛出,边界捕获。职责分离。

危险信号——停下来重新考虑

如果你发现以下任何一种情况,请移除捕获:

  • 纯业务逻辑函数中的 try/catch
  • 捕获后不做转换就重新抛出
  • 只记录日志并继续执行的 try/catch
  • 空的 catch 代码块
  • 类中的每个方法都使用 catch
  • 为了“防御性编程”而使用 try/catch
  • 捕获无法有意义地处理的错误

以上情况都意味着:让错误继续传播到真正的边界。

边界清单

梳理应用中的边界:

// Document where boundaries exist
const BOUNDARIES = {
  // Entry points
  HTTP: 'errorMiddleware in app.ts',
  GraphQL: 'formatError in apollo.ts',
  MessageQueue: 'errorHandler in consumer.ts',
  CronJobs: 'wrapWithErrorHandling in scheduler.ts',
  
  // Internal boundaries
  ExternalAPIs: [
    'PaymentGatewayAdapter',
    'EmailServiceAdapter',
    'SearchServiceAdapter',
  ],
  
  // UI boundaries
  React: 'ErrorBoundary components per feature',
  
  // Optional/degradable features
  Degradable: [
    'RecommendationService (fallback: empty)',
    'AnalyticsService (fallback: skip)',
  ],
};

常见借口(都不成立)

借口事实
“防御性编程是好事”防御性意味着验证输入,不意味着散布捕获。
“错误在哪发生,就在哪捕获”在边界处捕获,在发生处抛出。
“通过捕获并重新抛出添加上下文”通常只会增加噪音。边界本身就有上下文。
“防止故障级联”边界可以阻止级联。随机捕获会掩盖错误。
“组件独立性”组件抛出,边界捕获,组件依然独立。
“安全包装”包装会掩盖故障。应当快速失败。

快速参考

位置操作
HTTP 中间件✅ 捕获并转换为响应
消息处理程序✅ 捕获、确认/拒绝、记录日志
外部适配器✅ 捕获并转换为领域错误
React 错误边界✅ 捕获并显示备用 UI
服务方法❌ 让错误继续传播
仓库方法❌ 让错误继续传播(除非包装数据库错误)
工具函数❌ 让错误继续传播
纯业务逻辑❌ 让错误继续传播

核心要点

边界是架构性的。捕获是策略性的。

在上下文转换处设置 try/catch:HTTP 入口、外部适配器、UI 组件隔离、可选功能降级。绝不要在业务逻辑中设置。让错误继续传播到边界,以便在那里转换、记录日志并妥善处理。

评论

支持 Markdown