# DramaLing 錯誤處理指南 ## 錯誤處理策略 ### 分層錯誤處理 1. **API 層**: 捕獲並格式化錯誤 2. **業務邏輯層**: 處理業務規則錯誤 3. **UI 層**: 顯示用戶友好的錯誤信息 4. **全域層**: Error Boundary 捕獲未處理錯誤 ## 錯誤類型定義 ### 基礎錯誤類型 ```typescript // types/error.ts export class AppError extends Error { constructor( message: string, public code: string, public statusCode: number = 500, public isOperational: boolean = true ) { super(message) this.name = 'AppError' Error.captureStackTrace(this, this.constructor) } } export class ValidationError extends AppError { constructor(message: string, public fields?: Record) { super(message, 'VALIDATION_ERROR', 400) this.name = 'ValidationError' } } export class AuthenticationError extends AppError { constructor(message: string = '認證失敗') { super(message, 'AUTH_ERROR', 401) this.name = 'AuthenticationError' } } export class AuthorizationError extends AppError { constructor(message: string = '無權限訪問') { super(message, 'FORBIDDEN', 403) this.name = 'AuthorizationError' } } export class NotFoundError extends AppError { constructor(resource: string) { super(`${resource} 不存在`, 'NOT_FOUND', 404) this.name = 'NotFoundError' } } export class RateLimitError extends AppError { constructor(retryAfter: number) { super('請求過於頻繁', 'RATE_LIMIT', 429) this.name = 'RateLimitError' this.retryAfter = retryAfter } retryAfter: number } ``` ## API 錯誤處理 ### 統一錯誤回應格式 ```typescript // lib/api/error-handler.ts import { NextResponse } from 'next/server' import { AppError } from '@/types/error' interface ErrorResponse { error: { message: string code: string details?: any } timestamp: string path?: string } export function handleApiError(error: unknown, path?: string): NextResponse { console.error('API Error:', error) let statusCode = 500 let message = '伺服器錯誤' let code = 'INTERNAL_ERROR' let details = undefined if (error instanceof AppError) { statusCode = error.statusCode message = error.message code = error.code if (error instanceof ValidationError && error.fields) { details = error.fields } } else if (error instanceof Error) { message = error.message } const response: ErrorResponse = { error: { message, code, details }, timestamp: new Date().toISOString(), path } return NextResponse.json(response, { status: statusCode }) } ``` ### API Route 錯誤處理範例 ```typescript // app/api/flashcards/route.ts import { handleApiError } from '@/lib/api/error-handler' import { ValidationError } from '@/types/error' export async function POST(request: Request) { try { const body = await request.json() // 驗證輸入 if (!body.word) { throw new ValidationError('缺少必要欄位', { word: '單字不能為空' }) } // 業務邏輯 const flashcard = await createFlashcard(body) return NextResponse.json({ flashcard }, { status: 201 }) } catch (error) { return handleApiError(error, '/api/flashcards') } } ``` ## 前端錯誤處理 ### 全域 Error Boundary ```typescript // app/error.tsx 'use client' import { useEffect } from 'react' import { Button } from '@/components/ui/button' import { AlertCircle } from 'lucide-react' export default function Error({ error, reset, }: { error: Error & { digest?: string } reset: () => void }) { useEffect(() => { // 記錄錯誤到錯誤追蹤服務 console.error('Application Error:', error) // 可以發送到 Sentry 或其他錯誤追蹤服務 // Sentry.captureException(error) }, [error]) return (

發生錯誤

{error.message || '應用程式遇到了問題,請稍後再試'}

{process.env.NODE_ENV === 'development' && (
錯誤詳情
              {error.stack}
            
)}
) } ``` ### 組件級錯誤處理 ```typescript // components/error-boundary.tsx 'use client' import React from 'react' import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' import { AlertCircle } from 'lucide-react' interface ErrorBoundaryProps { children: React.ReactNode fallback?: React.ComponentType<{ error: Error; reset: () => void }> } interface ErrorBoundaryState { hasError: boolean error: Error | null } export class ErrorBoundary extends React.Component< ErrorBoundaryProps, ErrorBoundaryState > { constructor(props: ErrorBoundaryProps) { super(props) this.state = { hasError: false, error: null } } static getDerivedStateFromError(error: Error): ErrorBoundaryState { return { hasError: true, error } } componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { console.error('ErrorBoundary caught:', error, errorInfo) } reset = () => { this.setState({ hasError: false, error: null }) } render() { if (this.state.hasError && this.state.error) { const Fallback = this.props.fallback if (Fallback) { return } return ( 錯誤 {this.state.error.message} ) } return this.props.children } } ``` ## 表單錯誤處理 ### 使用 React Hook Form + Zod ```typescript // lib/validations/flashcard.ts import { z } from 'zod' export const flashcardSchema = z.object({ word: z.string().min(1, '單字不能為空').max(100, '單字過長'), translation: z.string().min(1, '翻譯不能為空'), context: z.string().optional(), example: z.string().optional(), difficulty: z.number().min(1).max(5), tags: z.array(z.string()).optional() }) export type FlashcardFormData = z.infer ``` ### 表單組件範例 ```typescript // components/flashcard/flashcard-form.tsx 'use client' import { useForm } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import { flashcardSchema, FlashcardFormData } from '@/lib/validations/flashcard' import { useState } from 'react' import { Alert, AlertDescription } from '@/components/ui/alert' export function FlashcardForm() { const [error, setError] = useState(null) const form = useForm({ resolver: zodResolver(flashcardSchema), defaultValues: { word: '', translation: '', difficulty: 3 } }) const onSubmit = async (data: FlashcardFormData) => { try { setError(null) const response = await fetch('/api/flashcards', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }) if (!response.ok) { const error = await response.json() throw new Error(error.error.message) } // 成功處理 } catch (error) { if (error instanceof Error) { setError(error.message) } else { setError('發生未知錯誤') } } } return (
{error && ( {error} )} {/* 表單欄位 */}
) } ``` ## 非同步錯誤處理 ### 使用 React Query ```typescript // hooks/use-flashcards.ts import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { toast } from '@/components/ui/use-toast' export function useFlashcards() { return useQuery({ queryKey: ['flashcards'], queryFn: async () => { const response = await fetch('/api/flashcards') if (!response.ok) { const error = await response.json() throw new Error(error.error.message) } return response.json() }, retry: 3, retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000) }) } export function useCreateFlashcard() { const queryClient = useQueryClient() return useMutation({ mutationFn: async (data: FlashcardFormData) => { const response = await fetch('/api/flashcards', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }) if (!response.ok) { const error = await response.json() throw new Error(error.error.message) } return response.json() }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['flashcards'] }) toast({ title: '成功', description: '詞卡已建立' }) }, onError: (error) => { toast({ title: '錯誤', description: error instanceof Error ? error.message : '建立失敗', variant: 'destructive' }) } }) } ``` ## Supabase 錯誤處理 ### 封裝 Supabase 客戶端 ```typescript // lib/supabase/error-handler.ts import { PostgrestError } from '@supabase/supabase-js' import { AppError, NotFoundError, ValidationError } from '@/types/error' export function handleSupabaseError(error: PostgrestError): never { console.error('Supabase Error:', error) // 處理常見錯誤碼 switch (error.code) { case '23505': // unique_violation throw new ValidationError('資料已存在') case '23503': // foreign_key_violation throw new ValidationError('關聯資料不存在') case '23502': // not_null_violation throw new ValidationError('缺少必要資料') case 'PGRST116': // not found throw new NotFoundError('資源') default: throw new AppError( error.message || '資料庫操作失敗', error.code, 500 ) } } // 使用範例 export async function getFlashcard(id: string) { const { data, error } = await supabase .from('flashcards') .select('*') .eq('id', id) .single() if (error) { handleSupabaseError(error) } return data } ``` ## 錯誤日誌與監控 ### 錯誤日誌記錄 ```typescript // lib/logger.ts type LogLevel = 'info' | 'warn' | 'error' class Logger { private log(level: LogLevel, message: string, data?: any) { const timestamp = new Date().toISOString() const logData = { timestamp, level, message, data } // 開發環境輸出到控制台 if (process.env.NODE_ENV === 'development') { console[level](message, data) } // 生產環境發送到日誌服務 if (process.env.NODE_ENV === 'production') { // 發送到 CloudWatch, Datadog, 等 this.sendToLogService(logData) } } private sendToLogService(logData: any) { // 實作日誌服務整合 } info(message: string, data?: any) { this.log('info', message, data) } warn(message: string, data?: any) { this.log('warn', message, data) } error(message: string, error?: any) { this.log('error', message, { error: error instanceof Error ? { message: error.message, stack: error.stack } : error }) } } export const logger = new Logger() ``` ## 用戶友好的錯誤信息 ### 錯誤信息映射 ```typescript // lib/error-messages.ts export const ERROR_MESSAGES: Record = { // 認證錯誤 'AUTH_ERROR': '請先登入', 'INVALID_CREDENTIALS': '帳號或密碼錯誤', 'EMAIL_NOT_CONFIRMED': '請先驗證您的 Email', // 驗證錯誤 'VALIDATION_ERROR': '輸入資料有誤', 'REQUIRED_FIELD': '此欄位為必填', // 網路錯誤 'NETWORK_ERROR': '網路連線失敗,請檢查您的網路', 'TIMEOUT': '請求超時,請稍後再試', // 業務錯誤 'QUOTA_EXCEEDED': '已達到使用上限', 'RATE_LIMIT': '操作過於頻繁,請稍後再試', // 預設錯誤 'UNKNOWN_ERROR': '發生未知錯誤,請稍後再試' } export function getUserFriendlyMessage(errorCode: string): string { return ERROR_MESSAGES[errorCode] || ERROR_MESSAGES['UNKNOWN_ERROR'] } ``` ## 測試錯誤處理 ### 單元測試範例 ```typescript // tests/error-handler.test.ts import { handleApiError } from '@/lib/api/error-handler' import { ValidationError, NotFoundError } from '@/types/error' describe('Error Handler', () => { it('should handle ValidationError correctly', () => { const error = new ValidationError('Invalid input', { email: 'Email 格式錯誤' }) const response = handleApiError(error) const body = JSON.parse(response.body) expect(response.status).toBe(400) expect(body.error.code).toBe('VALIDATION_ERROR') expect(body.error.details).toEqual({ email: 'Email 格式錯誤' }) }) it('should handle NotFoundError correctly', () => { const error = new NotFoundError('User') const response = handleApiError(error) expect(response.status).toBe(404) }) it('should handle unknown errors', () => { const error = new Error('Something went wrong') const response = handleApiError(error) expect(response.status).toBe(500) }) }) ``` ## 最佳實踐 1. **早期驗證**: 在處理前驗證輸入 2. **具體錯誤**: 提供明確的錯誤信息 3. **錯誤恢復**: 提供重試或替代方案 4. **日誌記錄**: 記錄所有錯誤供調試 5. **用戶友好**: 顯示易懂的錯誤信息 6. **安全考量**: 不暴露敏感信息 7. **監控告警**: 設置錯誤率監控