| name | error-handling |
| description | Enforce proper error handling patterns. Use when writing async code, API calls, or user-facing features. Covers try-catch, error boundaries, graceful degradation, and user feedback. |
| allowed-tools | Read, Glob, Grep, Edit, Write, Bash |
| license | MIT |
| metadata | [object Object] |
Error Handling Patterns
적절한 에러 처리 패턴을 강제하는 스킬입니다.
Core Principle
"에러는 숨기지 않고, 적절히 처리하고, 사용자에게 알린다." "Fail gracefully, recover when possible."
Rules
| 규칙 | 상태 | 설명 |
|---|---|---|
| 빈 catch 블록 금지 | 🔴 필수 | 최소 로깅 필수 |
| 사용자 친화적 메시지 | 🔴 필수 | 기술적 에러 메시지 노출 금지 |
| Error Boundary 사용 | 🔴 필수 (React) | 컴포넌트 에러 격리 |
| Graceful Degradation | 🟡 권장 | 부분 실패 시 대안 제공 |
기본 패턴
Try-Catch 올바른 사용
// ❌ BAD: 빈 catch 블록
try {
await fetchData();
} catch (e) {
// 아무것도 안 함 - 에러 무시
}
// ❌ BAD: 모든 에러 동일 처리
try {
await fetchData();
} catch (e) {
console.log('에러 발생'); // 정보 부족
}
// ✅ GOOD: 적절한 에러 처리
try {
await fetchData();
} catch (error) {
// 1. 에러 로깅 (개발자용)
console.error('fetchData failed:', error);
// 2. 에러 추적 서비스 전송
errorTracker.capture(error);
// 3. 사용자에게 알림
showToast('데이터를 불러오는데 실패했습니다. 다시 시도해주세요.');
// 4. 필요시 재시도 또는 대안 제공
return fallbackData;
}
에러 타입 구분
// ✅ GOOD: 에러 타입별 처리
async function fetchUser(id: string) {
try {
const response = await api.get(`/users/${id}`);
return response.data;
} catch (error) {
if (error instanceof NetworkError) {
// 네트워크 에러: 재시도 제안
showToast('네트워크 연결을 확인해주세요.');
return null;
}
if (error instanceof NotFoundError) {
// 404: 사용자 없음
showToast('사용자를 찾을 수 없습니다.');
return null;
}
if (error instanceof AuthError) {
// 인증 에러: 로그인 페이지로
router.push('/login');
return null;
}
// 예상치 못한 에러
console.error('Unexpected error:', error);
errorTracker.capture(error);
showToast('오류가 발생했습니다. 잠시 후 다시 시도해주세요.');
return null;
}
}
커스텀 에러 클래스
// errors.ts
export class AppError extends Error {
constructor(
message: string,
public code: string,
public statusCode?: number,
public isOperational: boolean = true
) {
super(message);
this.name = 'AppError';
}
}
export class ValidationError extends AppError {
constructor(message: string, public field?: string) {
super(message, 'VALIDATION_ERROR', 400);
this.name = 'ValidationError';
}
}
export class NetworkError extends AppError {
constructor(message: string = '네트워크 연결을 확인해주세요') {
super(message, 'NETWORK_ERROR', 0);
this.name = 'NetworkError';
}
}
export class NotFoundError extends AppError {
constructor(resource: string) {
super(`${resource}을(를) 찾을 수 없습니다`, 'NOT_FOUND', 404);
this.name = 'NotFoundError';
}
}
React Error Boundary
기본 Error Boundary
// ErrorBoundary.tsx
import { Component, ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback?: ReactNode;
onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
}
interface State {
hasError: boolean;
error?: Error;
}
export class ErrorBoundary extends Component<Props, State> {
state: State = { hasError: false };
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('Error caught by boundary:', error, errorInfo);
this.props.onError?.(error, errorInfo);
// 에러 추적 서비스로 전송
errorTracker.captureException(error, { extra: errorInfo });
}
render() {
if (this.state.hasError) {
return this.props.fallback || <DefaultErrorFallback error={this.state.error} />;
}
return this.props.children;
}
}
// 기본 폴백 UI
function DefaultErrorFallback({ error }: { error?: Error }) {
return (
<div className="error-fallback">
<h2>문제가 발생했습니다</h2>
<p>페이지를 새로고침하거나 잠시 후 다시 시도해주세요.</p>
<button onClick={() => window.location.reload()}>
새로고침
</button>
</div>
);
}
Error Boundary 사용
// 앱 전체 감싸기
function App() {
return (
<ErrorBoundary fallback={<FullPageError />}>
<Router>
<Routes />
</Router>
</ErrorBoundary>
);
}
// 특정 섹션만 감싸기
function Dashboard() {
return (
<div>
<Header />
<ErrorBoundary fallback={<ChartError />}>
<Chart data={data} />
</ErrorBoundary>
<ErrorBoundary fallback={<TableError />}>
<DataTable data={data} />
</ErrorBoundary>
</div>
);
}
Async 에러 처리
Promise 에러
// ❌ BAD: unhandled rejection
fetchData().then(data => setData(data));
// ✅ GOOD: catch 처리
fetchData()
.then(data => setData(data))
.catch(error => {
console.error('Failed to fetch:', error);
setError(error);
});
// ✅ BETTER: async/await
async function loadData() {
try {
const data = await fetchData();
setData(data);
} catch (error) {
console.error('Failed to fetch:', error);
setError(error);
}
}
여러 Promise 처리
// ❌ BAD: 하나라도 실패하면 전체 실패
const [users, posts] = await Promise.all([
fetchUsers(),
fetchPosts(),
]);
// ✅ GOOD: 개별 결과 처리
const results = await Promise.allSettled([
fetchUsers(),
fetchPosts(),
]);
const users = results[0].status === 'fulfilled' ? results[0].value : [];
const posts = results[1].status === 'fulfilled' ? results[1].value : [];
// 실패한 것만 로깅
results
.filter((r): r is PromiseRejectedResult => r.status === 'rejected')
.forEach(r => console.error('Failed:', r.reason));
Graceful Degradation
기능 저하 패턴
// ✅ GOOD: 실패 시 대안 제공
async function getRecommendations(userId: string) {
try {
// 1차: 개인화된 추천
return await fetchPersonalizedRecommendations(userId);
} catch (error) {
console.warn('Personalized recommendations failed:', error);
try {
// 2차: 인기 콘텐츠
return await fetchPopularContent();
} catch (error) {
console.warn('Popular content failed:', error);
// 3차: 캐시된 기본 추천
return getCachedDefaultRecommendations();
}
}
}
UI 대안 제공
function UserAvatar({ userId }: { userId: string }) {
const [imageError, setImageError] = useState(false);
const user = useUser(userId);
if (imageError || !user?.avatarUrl) {
// 이미지 로드 실패 시 대안
return (
<div className="avatar-placeholder">
{user?.name?.charAt(0) || '?'}
</div>
);
}
return (
<img
src={user.avatarUrl}
alt={user.name}
onError={() => setImageError(true)}
/>
);
}
사용자 친화적 메시지
메시지 매핑
const errorMessages: Record<string, string> = {
NETWORK_ERROR: '네트워크 연결을 확인해주세요.',
UNAUTHORIZED: '로그인이 필요합니다.',
FORBIDDEN: '접근 권한이 없습니다.',
NOT_FOUND: '요청한 정보를 찾을 수 없습니다.',
VALIDATION_ERROR: '입력 정보를 확인해주세요.',
RATE_LIMIT: '요청이 너무 많습니다. 잠시 후 다시 시도해주세요.',
SERVER_ERROR: '서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.',
DEFAULT: '오류가 발생했습니다. 다시 시도해주세요.',
};
function getUserFriendlyMessage(error: unknown): string {
if (error instanceof AppError) {
return errorMessages[error.code] || errorMessages.DEFAULT;
}
return errorMessages.DEFAULT;
}
🔴 금지: 기술적 메시지 노출
// ❌ BAD: 사용자에게 기술적 메시지 표시
showToast(error.message); // "TypeError: Cannot read property 'id' of undefined"
showToast(error.stack); // 스택 트레이스 노출
// ✅ GOOD: 친화적 메시지
showToast(getUserFriendlyMessage(error));
로깅 전략
// logger.ts
export const logger = {
error: (message: string, error: unknown, context?: object) => {
// 개발 환경: 콘솔 출력
if (process.env.NODE_ENV === 'development') {
console.error(message, error, context);
}
// 프로덕션: 에러 추적 서비스
errorTracker.captureException(error, {
tags: { message },
extra: context,
});
},
warn: (message: string, context?: object) => {
console.warn(message, context);
},
};
Checklist
코드 작성 시
- try-catch에 적절한 에러 처리 로직
- 빈 catch 블록 없음
- 에러 타입별 분기 처리
- 사용자 친화적 메시지 표시
- 에러 로깅/추적
React 컴포넌트
- Error Boundary 적용
- 로딩/에러 상태 UI
- 재시도 기능 제공
- 폴백 UI 구현
API 호출
- 네트워크 에러 처리
- 타임아웃 처리
- 재시도 로직 (필요시)
- 캐시 폴백 (필요시)