Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Use when implementing structured error handling in backend or frontend code. Triggers for: try-catch patterns, custom exception classes, global error handlers, error logging, user-friendly error messages, or API error responses. NOT for: business logic validation (use domain exceptions) or unrelated error types.
.claude/skills/aiskillstore-error-handling/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-05 | ✗→✓ | ▲ Improved | 115% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 58% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 100% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 80% | 0% |
| case-19 | ✗→✓ | ▲ Improved | 89% | 0% |
적절한 에러 처리 패턴을 강제하는 스킬입니다.
> "에러는 숨기지 않고, 적절히 처리하고, 사용자에게 알린다." > "Fail gracefully, recover when possible."
| 규칙 | 상태 | 설명 | |------|------|------| | 빈 catch 블록 금지 | 🔴 필수 | 최소 로깅 필수 | | 사용자 친화적 메시지 | 🔴 필수 | 기술적 에러 메시지 노출 금지 | | Error Boundary 사용 | 🔴 필수 (React) | 컴포넌트 에러 격리 | | Graceful Degradation | 🟡 권장 | 부분 실패 시 대안 제공 |
typescript// ❌ 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; }
typescript// ✅ 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; } }
typescript// 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'; } }
tsx// 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> ); }
tsx// 앱 전체 감싸기 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> ); }
typescript// ❌ 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); } }
typescript// ❌ 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));
typescript// ✅ 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(); } } }
tsxfunction 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)} /> ); }
typescriptconst 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; }
typescript// ❌ BAD: 사용자에게 기술적 메시지 표시 showToast(error.message); // "TypeError: Cannot read property 'id' of undefined" showToast(error.stack); // 스택 트레이스 노출 // ✅ GOOD: 친화적 메시지 showToast(getUserFriendlyMessage(error));
typescript// 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); }, };
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-05 | fail→pass | 11,783 | 15,744 | +34% | 1 | 1 | 0% | 2,163 | 4,640 | +115% | 0 | 0 | — |
case-06 | pass→pass | 17,211 | 13,481 | -22% | 1 | 1 | 0% | 2,288 | 4,446 | +94% | 0 | 0 | — |
case-01 | fail→fail | 22,142 | 26,172 | +18% | 1 | 1 | 0% | 4,560 | 7,535 | +65% | 0 | 0 | — |
case-02 | fail→pass | 17,514 | 20,203 | +15% | 1 | 1 | 0% | 4,218 | 6,646 | +58% | 0 | 0 | — |
case-03 | fail→pass | 19,311 | 18,660 | -3% | 1 | 1 | 0% | 2,634 | 5,256 | +100% | 0 | 0 | — |
case-04 | pass→pass | 22,512 | 16,754 | -26% | 1 | 1 | 0% | 2,719 | 5,112 | +88% | 0 | 0 | — |
case-07 | pass→pass | 16,546 | 9,991 | -40% | 1 | 1 | 0% | 2,471 | 4,676 | +89% | 0 | 0 | — |
case-08 | pass→pass | 15,190 | 20,923 | +38% | 1 | 1 | 0% | 3,028 | 6,427 | +112% | 0 | 0 | — |
case-09 | fail→pass | 14,297 | 17,579 | +23% | 1 | 1 | 0% | 2,909 | 5,239 | +80% | 0 | 0 | — |
case-10 | pass→pass | 20,540 | 10,352 | -50% | 1 | 1 | 0% | 2,968 | 4,753 | +60% | 0 | 0 | — |
case-11 | pass→pass | 12,500 | 6,510 | -48% | 1 | 1 | 0% | 2,363 | 3,933 | +66% | 0 | 0 | — |
case-12 | pass→pass | 12,086 | 5,609 | -54% | 1 | 1 | 0% | 1,554 | 3,724 | +140% | 0 | 0 | — |
case-13 | pass→pass | 11,610 | 9,591 | -17% | 1 | 1 | 0% | 2,223 | 3,655 | +64% | 0 | 0 | — |
case-14 | pass→pass | 9,251 | 15,564 | +68% | 1 | 1 | 0% | 1,861 | 4,511 | +142% | 0 | 0 | — |
case-15 | pass→pass | 15,288 | 24,595 | +61% | 1 | 1 | 0% | 3,181 | 7,067 | +122% | 0 | 0 | — |
case-16 | pass→pass | 13,350 | 9,248 | -31% | 1 | 1 | 0% | 2,681 | 4,937 | +84% | 0 | 0 | — |
case-17 | pass→pass | 20,131 | 23,462 | +17% | 1 | 1 | 0% | 3,390 | 6,762 | +99% | 0 | 0 | — |
case-18 | pass→pass | 20,489 | 12,640 | -38% | 1 | 1 | 0% | 2,900 | 5,176 | +78% | 0 | 0 | — |
case-19 | fail→pass | 14,651 | 12,175 | -17% | 1 | 1 | 0% | 1,945 | 3,672 | +89% | 0 | 0 | — |
case-20 | pass→pass | 21,019 | 11,437 | -46% | 1 | 1 | 0% | 1,650 | 3,992 | +142% | 0 | 0 | — |
case-21 | pass→pass | 17,665 | 15,224 | -14% | 1 | 1 | 0% | 2,464 | 5,989 | +143% | 0 | 0 | — |
case-22 | pass→pass | 16,084 | 17,108 | +6% | 1 | 1 | 0% | 2,037 | 5,301 | +160% | 0 | 0 | — |
case-23 | pass→pass | 18,775 | 15,598 | -17% | 1 | 1 | 0% | 3,733 | 6,188 | +66% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted. The headline lift of +22 percentage points is the difference between those two pass rates over the 23 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.