error-boundaries
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseError Boundaries
错误边界(Error Boundaries)
Overview
概述
Catch errors at logical boundaries, not random points in the call stack.
Error boundaries are strategic catch points that prevent cascading failures while enabling graceful degradation. Place them at architectural boundaries—not scattered throughout business logic.
在逻辑边界处捕获错误,而非调用栈中的随机位置。
错误边界是战略性的捕获点,可防止级联故障,同时实现优雅降级。应将其放置在架构边界处——而非分散在业务逻辑中。
When to Use
适用场景
- Designing application architecture
- Deciding where try/catch belongs
- Preventing one failure from crashing everything
- Implementing graceful degradation
- Isolating components from each other
- 应用架构设计
- 确定try/catch的合理位置
- 防止单个故障导致整体崩溃
- 实现优雅降级
- 组件间隔离
The Iron Rule
铁则
NEVER scatter try/catch randomly. Place catches at ARCHITECTURAL BOUNDARIES only.No exceptions:
- Not for "defensive programming"
- Not for "safety wrapper"
- Not for "just in case"
- Not for "the function might throw"
Boundaries are intentional. Random catches hide bugs.
绝不随意分散try/catch。仅在架构边界处放置捕获逻辑。无例外情况:
- 不用于“防御性编程”
- 不用于“安全包装”
- 不用于“以防万一”
- 不用于“函数可能抛出异常”的情况
边界是经过深思熟虑的。随机捕获会隐藏bug。
What Is a Boundary?
什么是边界?
Boundaries are points where context changes:
┌─────────────────────────────────────────────────────────────┐
│ 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 请求 → [边界] → 应用程序 │
│ 消息队列 → [边界] → 处理器 │
│ CLI 命令 → [边界] → 执行逻辑 │
│ 定时任务 → [边界] → 任务实例 │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 内部边界 │
│ 应用程序 → [边界] → 外部API │
│ 业务逻辑 → [边界] → 数据库 │
│ 核心代码 → [边界] → 第三方库 │
│ 父组件 → [边界] → 子组件 │
└─────────────────────────────────────────────────────────────┘Strategic Boundary Placement
边界的战略性放置
Entry Boundary: HTTP Controller
入口边界:HTTP控制器
typescript
// ✅ 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
}typescript
// ✅ 正确做法:顶层错误中间件
app.use(errorMiddleware);
function errorMiddleware(
error: Error,
req: Request,
res: Response,
next: NextFunction
) {
// 这是HTTP与应用程序之间的边界
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' });
}
// 记录未知错误,返回通用响应
logger.error('Unhandled error', { error, request: req.path });
return res.status(500).json({ error: 'Internal server error' });
}
// ❌ 错误做法:在每个控制器中使用try/catch
async function getUser(req: Request, res: Response) {
try {
const user = await userService.findById(req.params.id);
res.json(user);
} catch (error) {
// 分散式捕获 - 在所有控制器中重复出现
res.status(500).json({ error: 'Failed' });
}
}
// ✅ 正确做法:让错误传播到中间件
async function getUser(req: Request, res: Response) {
const user = await userService.findById(req.params.id);
res.json(user);
// 错误会传播到errorMiddleware
}Internal Boundary: External Service Adapter
内部边界:外部服务适配器
typescript
// ✅ 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 adaptertypescript
// ✅ 正确做法:自有代码与外部服务之间的边界
class PaymentGatewayAdapter {
async charge(amount: number, token: string): Promise<ChargeResult> {
try {
// 外部调用 - 这是一个边界
const response = await this.stripeClient.charges.create({
amount,
source: token,
});
return this.mapToChargeResult(response);
} catch (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 });
}
}
}
// ❌ 错误做法:让Stripe错误泄露到业务逻辑中
// ❌ 错误做法:在OrderService中捕获而非适配器中UI Boundary: React Error Boundary
UI边界:React Error Boundary
tsx
// ✅ 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>
);
}tsx
// ✅ 正确做法:组件级错误隔离
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) {
// 上报到监控服务
errorService.report(error, errorInfo);
}
render() {
if (this.state.hasError) {
return this.props.fallback || <DefaultErrorUI />;
}
return this.props.children;
}
}
// 使用方式:功能模块之间相互隔离
function App() {
return (
<Layout>
<ErrorBoundary fallback={<DashboardError />}>
<Dashboard />
</ErrorBoundary>
<ErrorBoundary fallback={<SidebarError />}>
<Sidebar />
</ErrorBoundary>
{/* 侧边栏错误不会导致仪表盘崩溃 */}
</Layout>
);
}The Boundary Checklist
边界检查清单
Before adding try/catch, verify:
| Question | If No... |
|---|---|
| Is this a context transition point? | Don't catch here |
| Would catching prevent meaningful propagation? | Don't catch here |
| Can I translate to a meaningful domain error? | Don't catch here |
| Is there a specific recovery action? | Don't catch here |
| Does the boundary change ownership? | Don't catch here |
添加try/catch之前,请验证以下内容:
| 问题 | 如果答案为否... |
|---|---|
| 这是上下文转换节点吗? | 不要在此处捕获 |
| 捕获会阻碍有意义的错误传播吗? | 不要在此处捕获 |
| 我能将其转换为有意义的领域错误吗? | 不要在此处捕获 |
| 有具体的恢复操作吗? | 不要在此处捕获 |
| 边界是否涉及所有权变更? | 不要在此处捕获 |
Correct Propagation Pattern
正确的错误传播模式
Let errors propagate through business logic:
typescript
// ✅ 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
}
}
}让错误在业务逻辑中传播:
typescript
// ✅ 正确做法:服务层无随机捕获
class OrderService {
async createOrder(data: OrderData): Promise<Order> {
// 验证(可能抛出ValidationError)
this.validator.validate(data);
// 获取用户(可能抛出NotFoundError)
const user = await this.userRepo.findById(data.userId);
// 检查业务规则(可能抛出BusinessRuleError)
this.rules.assertCanCreateOrder(user, data);
// 处理支付(可能从适配器抛出PaymentError)
const payment = await this.paymentAdapter.charge(data.amount, user.paymentToken);
// 创建订单(可能从仓库抛出DatabaseError)
const order = await this.orderRepo.create({
...data,
paymentId: payment.id,
});
return order;
// 所有错误都会传播到控制器边界
}
}
// ❌ 错误做法:服务层中的防御性try/catch
class OrderService {
async createOrder(data: OrderData): Promise<Order | null> {
try {
// ... 相同逻辑 ...
return order;
} catch (error) {
logger.error('Order creation failed', error);
return null; // 丢失上下文,隐藏故障
}
}
}Graceful Degradation at Boundaries
边界处的优雅降级
Boundaries can provide fallbacks:
typescript
// ✅ 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 };
}
}Key distinction: This is a boundary between "required" and "optional" features.
It's NOT random defensive programming—it's intentional graceful degradation.
边界可以提供回退方案:
typescript
// ✅ 正确做法:推荐模块边界处的优雅降级
class ProductPage {
async load(productId: string) {
// 核心数据 - 必须成功
const product = await this.productService.getById(productId);
// 推荐内容 - 可以优雅失败
let recommendations: Product[] = [];
try {
recommendations = await this.recommendationService.getFor(productId);
} catch (error) {
// 记录日志但不导致页面加载失败
logger.warn('Recommendations unavailable', { productId, error });
// 空推荐列表是可接受的回退方案
}
return { product, recommendations };
}
}关键区别: 这是“必需”与“可选”功能之间的边界。这不是随机的防御性编程——而是有意设计的优雅降级。
Pressure Resistance Protocol
压力应对准则
1. "Wrap Everything in Try/Catch for Safety"
1. “为了安全,用Try/Catch包裹所有代码”
Pressure: "Be defensive, catch all errors"
Response: Scattered catches hide bugs and prevent proper handling at boundaries. Errors propagate for a reason.
Action: Remove interior catches. Handle at boundaries only.
压力来源: “要防御性编程,捕获所有错误”
回应: 分散式捕获会隐藏bug,阻碍在边界处的正确处理。错误传播是有意义的。
行动: 移除内部的捕获逻辑,仅在边界处处理。
2. "The Function Might Throw"
2. “这个函数可能抛出异常”
Pressure: "I should catch just in case"
Response: That's what boundaries are for. Business logic shouldn't know about error handling.
Action: Let it throw. Boundary will catch.
压力来源: “我应该以防万一进行捕获”
回应: 这正是边界的作用所在。业务逻辑不应该处理错误。
行动: 让它抛出异常,边界会进行捕获。
3. "I Want to Add Context to Errors"
3. “我想为错误添加上下文信息”
Pressure: "Catch, add info, re-throw"
Response: Only if you're adding genuinely useful context. Most catch-and-rethrow just adds noise.
Action: Only wrap if context is truly lost otherwise. Usually it isn't.
压力来源: “捕获错误,添加信息,重新抛出”
回应: 仅当你能添加真正有用的上下文时才这么做。大多数捕获后重新抛出的操作只会增加冗余信息。
行动: 仅当上下文确实会丢失时才进行包装。通常情况下不会。
4. "Each Component Should Handle Its Errors"
4. “每个组件都应该处理自己的错误”
Pressure: "Encapsulation means local handling"
Response: Components should THROW appropriate errors. CATCHING is for boundaries.
Action: Component throws. Boundary catches. Separation of concerns.
压力来源: “封装意味着本地处理”
回应: 组件应该抛出合适的错误。捕获是边界的职责。
行动: 组件抛出错误,边界进行捕获。关注点分离。
Red Flags - STOP and Reconsider
危险信号 - 立即停止并重新考虑
If you notice ANY of these, remove the catch:
- try/catch in pure business logic functions
- Catching and re-throwing without translation
- try/catch that just logs and continues
- Empty catch blocks
- Catch in every method of a class
- try/catch for "defensive programming"
- Catching errors you can't meaningfully handle
All of these mean: Let the error propagate to a real boundary.
如果你注意到以下任何一种情况,请移除捕获逻辑:
- 纯业务逻辑函数中的try/catch
- 捕获后重新抛出但未进行错误转换
- 仅记录日志后继续执行的try/catch
- 空的catch块
- 类中每个方法都有try/catch
- 用于“防御性编程”的try/catch
- 捕获你无法有效处理的错误
所有这些情况都意味着:让错误传播到真正的边界处。
Boundary Inventory
边界清单
Map your application's boundaries:
typescript
// 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)',
],
};梳理你的应用程序边界:
typescript
// 记录边界所在位置
const BOUNDARIES = {
// 入口点
HTTP: 'errorMiddleware in app.ts',
GraphQL: 'formatError in apollo.ts',
MessageQueue: 'errorHandler in consumer.ts',
CronJobs: 'wrapWithErrorHandling in scheduler.ts',
// 内部边界
ExternalAPIs: [
'PaymentGatewayAdapter',
'EmailServiceAdapter',
'SearchServiceAdapter',
],
// UI边界
React: 'ErrorBoundary components per feature',
// 可选/可降级功能
Degradable: [
'RecommendationService (fallback: empty)',
'AnalyticsService (fallback: skip)',
],
};Common Rationalizations (All Invalid)
常见的无效借口
| Excuse | Reality |
|---|---|
| "Defensive programming is good" | Defensive = validate inputs. Not = scatter catches. |
| "Catch errors where they occur" | Catch at boundaries. Throw where they occur. |
| "Add context with catch-rethrow" | Usually adds noise. Boundaries have context. |
| "Prevent cascading failures" | Boundaries prevent cascades. Random catches hide bugs. |
| "Component independence" | Components throw. Boundaries catch. Still independent. |
| "Safety wrapper" | Wrappers hide failures. Fail fast instead. |
| 借口 | 事实 |
|---|---|
| “防御性编程是好的” | 防御性编程 = 验证输入。而非 = 分散捕获逻辑。 |
| “在错误发生的位置捕获” | 在边界处捕获,在发生位置抛出。 |
| “通过捕获-重新抛出添加上下文” | 通常只会增加冗余信息。边界已经具备上下文。 |
| “防止级联故障” | 边界才能防止级联故障。随机捕获会隐藏bug。 |
| “组件独立性” | 组件抛出错误,边界捕获。仍然保持独立。 |
| “安全包装” | 包装会隐藏故障。应该快速失败。 |
Quick Reference
快速参考
| Location | Action |
|---|---|
| HTTP middleware | ✅ Catch and translate to responses |
| Message handler | ✅ Catch, ack/nack, log |
| External adapter | ✅ Catch and translate to domain errors |
| React error boundary | ✅ Catch and show fallback UI |
| Service method | ❌ Let errors propagate |
| Repository method | ❌ Let errors propagate (unless wrapping DB errors) |
| Utility function | ❌ Let errors propagate |
| Pure business logic | ❌ Let errors propagate |
| 位置 | 操作 |
|---|---|
| HTTP中间件 | ✅ 捕获并转换为响应 |
| 消息处理器 | ✅ 捕获、确认/拒绝、记录日志 |
| 外部适配器 | ✅ 捕获并转换为领域错误 |
| React错误边界 | ✅ 捕获并显示回退UI |
| 服务方法 | ❌ 让错误传播 |
| 仓库方法 | ❌ 让错误传播(除非包装数据库错误) |
| 工具函数 | ❌ 让错误传播 |
| 纯业务逻辑 | ❌ 让错误传播 |
The Bottom Line
核心要点
Boundaries are architectural. Catches are strategic.
Place try/catch at context transitions: HTTP entry, external adapters, UI component isolation, optional feature degradation. Never in business logic. Let errors propagate to boundaries where they can be translated, logged, and handled appropriately.
边界是架构层面的设计,捕获是战略性的操作。
将try/catch放置在上下文转换节点:HTTP入口、外部适配器、UI组件隔离、可选功能降级处。绝不要在业务逻辑中使用。让错误传播到边界处,在那里可以进行转换、记录和适当处理。