关于 error-boundaries
error-boundaries 是一项软件架构技能,说明如何在明确的边界捕获和处理错误,而不是在业务逻辑中随处分散捕获。它面向设计应用故障处理与组件降级策略的开发者。
主要功能
- 01识别架构中的错误边界
- 02指导捕获位置与错误传播
- 03说明边界处的优雅降级
- 04提供检查清单和边界示例
适用场景
- 01规划 HTTP 或命令行入口的错误处理
- 02隔离外部服务适配器中的故障
- 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 组件隔离、可选功能降级。绝不要在业务逻辑中设置。让错误继续传播到边界,以便在那里转换、记录日志并妥善处理。