エラーハンドリング詳細ガイド
Supabase実践アーキテクチャパターン 1.0版 株式会社アイティードゥ 2025年6月2日
目次
エラー分類と対応戦略
エラー分類体系
export enum ErrorCategory {
AUTHENTICATION = 'authentication',
AUTHORIZATION = 'authorization',
VALIDATION = 'validation',
DATABASE = 'database',
NETWORK = 'network',
BUSINESS_LOGIC = 'business_logic',
SYSTEM = 'system',
UNKNOWN = 'unknown'
}
export enum ErrorSeverity {
LOW = 'low',
MEDIUM = 'medium',
HIGH = 'high',
CRITICAL = 'critical'
}
export interface AppError {
id: string
category: ErrorCategory
severity: ErrorSeverity
code: string
message: string
userMessage: string
details?: any
timestamp: Date
context?: Record<string, any>
recoverable: boolean
retryable: boolean
}
包括的エラーハンドラー
export class ErrorHandler {
private static instance: ErrorHandler
private errorLog: AppError[] = []
private errorCallbacks: Map<ErrorCategory, Array<(error: AppError) => void>> = new Map()
static getInstance(): ErrorHandler {
if (!ErrorHandler.instance) {
ErrorHandler.instance = new ErrorHandler()
}
return ErrorHandler.instance
}
// エラー処理メイン関数
handleError(error: any, context?: Record<string, any>): AppError {
const appError = this.categorizeError(error, context)
this.logError(appError)
this.notifyCallbacks(appError)
// 重要度に応じた追加処理
if (appError.severity === ErrorSeverity.CRITICAL) {
this.handleCriticalError(appError)
}
return appError
}
// エラー分類ロジック
private categorizeError(error: any, context?: Record<string, any>): AppError {
const errorId = this.generateErrorId()
const timestamp = new Date()
// Supabaseエラーの分類
if (error?.code) {
return this.handleSupabaseError(error, errorId, timestamp, context)
}
// ネットワークエラーの分類
if (error instanceof TypeError && error.message.includes('fetch')) {
return this.createAppError({
id: errorId,
category: ErrorCategory.NETWORK,
severity: ErrorSeverity.MEDIUM,
code: 'NETWORK_ERROR',
message: error.message,
userMessage: 'ネットワーク接続に問題があります。しばらく待ってから再試行してください。',
timestamp,
context,
recoverable: true,
retryable: true
})
}
// 一般的なJavaScriptエラー
if (error instanceof Error) {
return this.createAppError({
id: errorId,
category: ErrorCategory.SYSTEM,
severity: ErrorSeverity.HIGH,
code: 'JAVASCRIPT_ERROR',
message: error.message,
userMessage: 'システムエラーが発生しました。しばらく待ってから再試行してください。',
timestamp,
context: { ...context, stack: error.stack },
recoverable: false,
retryable: false
})
}
// 未知のエラー
return this.createAppError({
id: errorId,
category: ErrorCategory.UNKNOWN,
severity: ErrorSeverity.HIGH,
code: 'UNKNOWN_ERROR',
message: String(error),
userMessage: '予期しないエラーが発生しました。',
timestamp,
context,
recoverable: false,
retryable: false
})
}
// Supabase固有エラーの処理
private handleSupabaseError(error: any, id: string, timestamp: Date, context?: Record<string, any>): AppError {
const errorMappings = {
// 認証エラー
'invalid_credentials': {
category: ErrorCategory.AUTHENTICATION,
severity: ErrorSeverity.LOW,
userMessage: 'メールアドレスまたはパスワードが正しくありません。',
recoverable: true,
retryable: true
},
'email_not_confirmed': {
category: ErrorCategory.AUTHENTICATION,
severity: ErrorSeverity.MEDIUM,
userMessage: 'メールアドレスの確認が完了していません。確認メールをご確認ください。',
recoverable: true,
retryable: false
},
'too_many_requests': {
category: ErrorCategory.SYSTEM,
severity: ErrorSeverity.MEDIUM,
userMessage: 'リクエストが多すぎます。しばらく待ってから再試行してください。',
recoverable: true,
retryable: true
},
// データベースエラー
'23505': { // UNIQUE_VIOLATION
category: ErrorCategory.VALIDATION,
severity: ErrorSeverity.LOW,
userMessage: 'この値は既に使用されています。',
recoverable: true,
retryable: false
},
'23503': { // FOREIGN_KEY_VIOLATION
category: ErrorCategory.VALIDATION,
severity: ErrorSeverity.MEDIUM,
userMessage: '関連するデータが見つかりません。',
recoverable: true,
retryable: false
},
'42501': { // INSUFFICIENT_PRIVILEGE
category: ErrorCategory.AUTHORIZATION,
severity: ErrorSeverity.HIGH,
userMessage: 'この操作を実行する権限がありません。',
recoverable: false,
retryable: false
},
// PostgreSQLエラー
'42P01': { // UNDEFINED_TABLE
category: ErrorCategory.DATABASE,
severity: ErrorSeverity.CRITICAL,
userMessage: 'システムエラーが発生しました。管理者にお問い合わせください。',
recoverable: false,
retryable: false
}
}
const mapping = errorMappings[error.code] || errorMappings[error.error_code] || {
category: ErrorCategory.UNKNOWN,
severity: ErrorSeverity.MEDIUM,
userMessage: 'エラーが発生しました。',
recoverable: false,
retryable: false
}
return this.createAppError({
id,
category: mapping.category,
severity: mapping.severity,
code: error.code || error.error_code || 'SUPABASE_ERROR',
message: error.message || 'Supabase error occurred',
userMessage: mapping.userMessage,
timestamp,
context: { ...context, originalError: error },
recoverable: mapping.recoverable,
retryable: mapping.retryable
})
}
private createAppError(params: Omit<AppError, 'details'>): AppError {
return {
...params,
details: params.context
}
}
private generateErrorId(): string {
return `err_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
}
private logError(error: AppError): void {
this.errorLog.push(error)
// コンソールログ
const logLevel = this.getLogLevel(error.severity)
console[logLevel](`[${error.category}] ${error.code}: ${error.message}`, {
id: error.id,
timestamp: error.timestamp,
context: error.context
})
// 外部ログサービスへの送信
this.sendToExternalLogging(error)
}
private getLogLevel(severity: ErrorSeverity): 'log' | 'warn' | 'error' {
switch (severity) {
case ErrorSeverity.LOW:
return 'log'
case ErrorSeverity.MEDIUM:
return 'warn'
case ErrorSeverity.HIGH:
case ErrorSeverity.CRITICAL:
return 'error'
}
}
private sendToExternalLogging(error: AppError): void {
// 外部ログサービス(例:Sentry、LogRocket等)への送信
if (typeof window !== 'undefined' && (window as any).Sentry) {
(window as any).Sentry.captureException(new Error(error.message), {
tags: {
category: error.category,
severity: error.severity,
code: error.code
},
extra: error.context
})
}
}
private handleCriticalError(error: AppError): void {
// 重要なエラーの場合の追加処理
console.error('CRITICAL ERROR DETECTED:', error)
// 管理者への通知
this.notifyAdministrators(error)
// フェイルセーフ処理
this.executeFailsafe(error)
}
private notifyAdministrators(error: AppError): void {
// 管理者通知ロジック(メール、Slack等)
console.log('Notifying administrators of critical error:', error.id)
}
private executeFailsafe(error: AppError): void {
// フェイルセーフ処理(緊急時の安全な状態への移行)
console.log('Executing failsafe procedures for error:', error.id)
}
private notifyCallbacks(error: AppError): void {
const callbacks = this.errorCallbacks.get(error.category) || []
callbacks.forEach(callback => {
try {
callback(error)
} catch (callbackError) {
console.error('Error in error callback:', callbackError)
}
})
}
// コールバック登録
onError(category: ErrorCategory, callback: (error: AppError) => void): void {
if (!this.errorCallbacks.has(category)) {
this.errorCallbacks.set(category, [])
}
this.errorCallbacks.get(category)!.push(callback)
}
// エラー統計取得
getErrorStats(timeRange?: { start: Date; end: Date }): Record<string, any> {
let filteredErrors = this.errorLog
if (timeRange) {
filteredErrors = this.errorLog.filter(error =>
error.timestamp >= timeRange.start && error.timestamp <= timeRange.end
)
}
const stats = {
total: filteredErrors.length,
byCategory: {} as Record<string, number>,
bySeverity: {} as Record<string, number>,
byCode: {} as Record<string, number>
}
filteredErrors.forEach(error => {
stats.byCategory[error.category] = (stats.byCategory[error.category] || 0) + 1
stats.bySeverity[error.severity] = (stats.bySeverity[error.severity] || 0) + 1
stats.byCode[error.code] = (stats.byCode[error.code] || 0) + 1
})
return stats
}
}
認証・認可エラー
認証エラーハンドラー
export class AuthErrorHandler {
static handleAuthError(error: any): {
shouldRetry: boolean
shouldRedirect: boolean
redirectUrl?: string
userMessage: string
actionRequired?: string
} {
switch (error.message || error.error_description) {
case 'Invalid login credentials':
return {
shouldRetry: true,
shouldRedirect: false,
userMessage: 'メールアドレスまたはパスワードが正しくありません。再度お試しください。',
actionRequired: 'check_credentials'
}
case 'Email not confirmed':
return {
shouldRetry: false,
shouldRedirect: false,
userMessage: 'メールアドレスの確認が必要です。確認メールをご確認ください。',
actionRequired: 'email_confirmation'
}
case 'Too many requests':
return {
shouldRetry: true,
shouldRedirect: false,
userMessage: 'ログイン試行回数が上限に達しました。15分後に再度お試しください。',
actionRequired: 'wait_and_retry'
}
case 'User not found':
return {
shouldRetry: true,
shouldRedirect: false,
userMessage: 'このメールアドレスは登録されていません。新規登録しますか?',
actionRequired: 'suggest_signup'
}
case 'Invalid refresh token':
case 'JWT expired':
return {
shouldRetry: false,
shouldRedirect: true,
redirectUrl: '/login',
userMessage: 'セッションが期限切れです。再度ログインしてください。',
actionRequired: 'relogin'
}
case 'Email already registered':
return {
shouldRetry: false,
shouldRedirect: false,
userMessage: 'このメールアドレスは既に登録されています。ログインしますか?',
actionRequired: 'suggest_login'
}
default:
return {
shouldRetry: false,
shouldRedirect: false,
userMessage: '認証エラーが発生しました。しばらく待ってから再試行してください。',
actionRequired: 'contact_support'
}
}
}
// 自動リトライ機能付き認証
static async authenticateWithRetry(
authFunction: () => Promise<any>,
maxRetries: number = 3,
baseDelay: number = 1000
): Promise<any> {
let lastError: any
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await authFunction()
} catch (error) {
lastError = error
const errorInfo = this.handleAuthError(error)
if (!errorInfo.shouldRetry || attempt === maxRetries) {
throw error
}
// 指数バックオフでリトライ
const delay = baseDelay * Math.pow(2, attempt - 1)
await new Promise(resolve => setTimeout(resolve, delay))
console.log(`Authentication attempt ${attempt} failed, retrying in ${delay}ms...`)
}
}
throw lastError
}
}
// 使用例
export async function loginWithErrorHandling(email: string, password: string) {
try {
return await AuthErrorHandler.authenticateWithRetry(async () => {
const { data, error } = await supabase.auth.signInWithPassword({
email,
password
})
if (error) throw error
return data
})
} catch (error) {
const errorHandler = ErrorHandler.getInstance()
const appError = errorHandler.handleError(error, {
operation: 'login',
email: email
})
const authErrorInfo = AuthErrorHandler.handleAuthError(error)
return {
success: false,
error: appError,
authInfo: authErrorInfo
}
}
}
データベースエラー
データベースエラー専用ハンドラー
export class DatabaseErrorHandler {
static handleDatabaseError(error: any): {
isUserError: boolean
canRetry: boolean
userMessage: string
technicalDetails: string
suggestedAction?: string
} {
// PostgreSQLエラーコード別処理
const postgresErrorMap: Record<string, any> = {
// データ整合性エラー
'23505': { // unique_violation
isUserError: true,
canRetry: false,
userMessage: 'この値は既に使用されています。別の値を入力してください。',
technicalDetails: 'Unique constraint violation',
suggestedAction: 'change_input'
},
'23503': { // foreign_key_violation
isUserError: true,
canRetry: false,
userMessage: '選択された項目が見つかりません。画面を更新して再度お試しください。',
technicalDetails: 'Foreign key constraint violation',
suggestedAction: 'refresh_data'
},
'23514': { // check_violation
isUserError: true,
canRetry: false,
userMessage: '入力された値が有効範囲外です。正しい値を入力してください。',
technicalDetails: 'Check constraint violation',
suggestedAction: 'validate_input'
},
// 権限エラー
'42501': { // insufficient_privilege
isUserError: false,
canRetry: false,
userMessage: 'この操作を実行する権限がありません。',
technicalDetails: 'Insufficient privileges',
suggestedAction: 'contact_admin'
},
// スキーマエラー
'42P01': { // undefined_table
isUserError: false,
canRetry: false,
userMessage: 'システムエラーが発生しました。管理者にお問い合わせください。',
technicalDetails: 'Table does not exist',
suggestedAction: 'contact_support'
},
'42703': { // undefined_column
isUserError: false,
canRetry: false,
userMessage: 'システムエラーが発生しました。管理者にお問い合わせください。',
technicalDetails: 'Column does not exist',
suggestedAction: 'contact_support'
},
// 接続・トランザクションエラー
'08006': { // connection_failure
isUserError: false,
canRetry: true,
userMessage: 'データベース接続に問題があります。しばらく待ってから再試行してください。',
technicalDetails: 'Connection failure',
suggestedAction: 'retry_later'
},
'40001': { // serialization_failure
isUserError: false,
canRetry: true,
userMessage: '処理が競合しました。しばらく待ってから再試行してください。',
technicalDetails: 'Serialization failure',
suggestedAction: 'retry_with_delay'
}
}
const errorCode = error.code || error.error_code
const mapping = postgresErrorMap[errorCode]
if (mapping) {
return {
...mapping,
technicalDetails: `${mapping.technicalDetails} (Code: ${errorCode})`
}
}
// PostgRESTエラーの処理
if (error.hint || error.details) {
return {
isUserError: false,
canRetry: false,
userMessage: 'データ処理中にエラーが発生しました。',
technicalDetails: `PostgREST Error: ${error.message}. Hint: ${error.hint}. Details: ${error.details}`,
suggestedAction: 'contact_support'
}
}
// 一般的なデータベースエラー
return {
isUserError: false,
canRetry: true,
userMessage: 'データベースエラーが発生しました。しばらく待ってから再試行してください。',
technicalDetails: error.message || 'Unknown database error',
suggestedAction: 'retry_later'
}
}
// リトライ機能付きデータベース操作
static async executeWithRetry<T>(
operation: () => Promise<{ data: T | null; error: any }>,
maxRetries: number = 3,
baseDelay: number = 1000
): Promise<T> {
let lastError: any
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const { data, error } = await operation()
if (error) {
const errorInfo = this.handleDatabaseError(error)
if (!errorInfo.canRetry || attempt === maxRetries) {
throw error
}
lastError = error
// 指数バックオフ
const delay = baseDelay * Math.pow(2, attempt - 1)
await new Promise(resolve => setTimeout(resolve, delay))
console.log(`Database operation attempt ${attempt} failed, retrying in ${delay}ms...`)
continue
}
if (data === null) {
throw new Error('No data returned from database operation')
}
return data
} catch (error) {
if (attempt === maxRetries) {
throw error
}
lastError = error
}
}
throw lastError
}
}
// 使用例:安全なデータベース操作
export async function safeCreateUser(userData: any) {
try {
const result = await DatabaseErrorHandler.executeWithRetry(async () => {
return await supabase
.from('users')
.insert(userData)
.select()
.single()
})
return { success: true, data: result }
} catch (error) {
const errorHandler = ErrorHandler.getInstance()
const appError = errorHandler.handleError(error, {
operation: 'create_user',
userData: { ...userData, password: '[REDACTED]' }
})
const dbErrorInfo = DatabaseErrorHandler.handleDatabaseError(error)
return {
success: false,
error: appError,
dbInfo: dbErrorInfo
}
}
}
リアルタイム機能エラー
WebSocketエラーハンドラー
export class RealtimeErrorHandler {
private reconnectAttempts: number = 0
private maxReconnectAttempts: number = 5
private reconnectDelay: number = 1000
private subscriptions: Map<string, any> = new Map()
handleRealtimeError(error: any, channelName: string): {
shouldReconnect: boolean
delay: number
userMessage: string
} {
switch (error.type || error.code) {
case 'WEBSOCKET_CLOSED':
case 'CONNECTION_LOST':
return {
shouldReconnect: this.reconnectAttempts < this.maxReconnectAttempts,
delay: this.calculateReconnectDelay(),
userMessage: 'リアルタイム接続が切断されました。再接続を試行しています...'
}
case 'SUBSCRIPTION_FAILED':
return {
shouldReconnect: true,
delay: 2000,
userMessage: 'リアルタイム機能の初期化に失敗しました。再試行しています...'
}
case 'AUTHENTICATION_FAILED':
return {
shouldReconnect: false,
delay: 0,
userMessage: 'リアルタイム機能の認証に失敗しました。再ログインが必要です。'
}
case 'RATE_LIMITED':
return {
shouldReconnect: true,
delay: 10000, // 10秒待機
userMessage: 'リアルタイム機能の使用制限に達しました。しばらく待ってから再接続します。'
}
default:
return {
shouldReconnect: this.reconnectAttempts < this.maxReconnectAttempts,
delay: this.calculateReconnectDelay(),
userMessage: 'リアルタイム機能でエラーが発生しました。再接続を試行しています...'
}
}
}
private calculateReconnectDelay(): number {
// 指数バックオフ + ジッター
const baseDelay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts)
const jitter = Math.random() * 1000
return Math.min(baseDelay + jitter, 30000) // 最大30秒
}
async createResilientSubscription(
supabase: any,
channelName: string,
config: any,
onData: (payload: any) => void,
onError?: (error: any) => void
) {
const createSubscription = () => {
const channel = supabase
.channel(channelName, config)
.on('postgres_changes', config.listenConfig, onData)
.on('error', (error: any) => {
console.error(`Realtime error on channel ${channelName}:`, error)
const errorInfo = this.handleRealtimeError(error, channelName)
if (onError) {
onError({ ...error, ...errorInfo })
}
if (errorInfo.shouldReconnect) {
this.scheduleReconnection(channelName, createSubscription, errorInfo.delay)
}
})
.subscribe((status: string) => {
console.log(`Channel ${channelName} status:`, status)
if (status === 'SUBSCRIBED') {
this.reconnectAttempts = 0 // リセット
} else if (status === 'CLOSED' || status === 'CHANNEL_ERROR') {
this.reconnectAttempts++
const errorInfo = this.handleRealtimeError(
{ type: status },
channelName
)
if (errorInfo.shouldReconnect) {
this.scheduleReconnection(channelName, createSubscription, errorInfo.delay)
}
}
})
this.subscriptions.set(channelName, channel)
return channel
}
return createSubscription()
}
private scheduleReconnection(
channelName: string,
reconnectFunction: () => any,
delay: number
) {
console.log(`Scheduling reconnection for ${channelName} in ${delay}ms`)
setTimeout(() => {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
console.log(`Attempting to reconnect ${channelName} (attempt ${this.reconnectAttempts + 1})`)
reconnectFunction()
} else {
console.error(`Max reconnection attempts reached for ${channelName}`)
}
}, delay)
}
// 全サブスクリプションのクリーンアップ
cleanupSubscriptions() {
for (const [channelName, channel] of this.subscriptions) {
try {
channel.unsubscribe()
console.log(`Unsubscribed from ${channelName}`)
} catch (error) {
console.error(`Error unsubscribing from ${channelName}:`, error)
}
}
this.subscriptions.clear()
this.reconnectAttempts = 0
}
}
// 使用例
export function useResilientRealtime(supabase: any, tableName: string) {
const realtimeHandler = new RealtimeErrorHandler()
const subscribeToTable = (onData: (payload: any) => void) => {
return realtimeHandler.createResilientSubscription(
supabase,
`table_${tableName}`,
{
listenConfig: {
event: '*',
schema: 'public',
table: tableName
}
},
onData,
(error) => {
// ユーザーにエラー状況を通知
console.warn('Realtime error:', error.userMessage)
}
)
}
return {
subscribeToTable,
cleanup: () => realtimeHandler.cleanupSubscriptions()
}
}
ネットワーク・接続エラー
ネットワークエラーハンドラー
export class NetworkErrorHandler {
private static isOnline: boolean = navigator.onLine
private static onlineListeners: Array<() => void> = []
private static offlineListeners: Array<() => void> = []
static {
// ネットワーク状態監視
window.addEventListener('online', () => {
this.isOnline = true
this.onlineListeners.forEach(listener => listener())
})
window.addEventListener('offline', () => {
this.isOnline = false
this.offlineListeners.forEach(listener => listener())
})
}
static handleNetworkError(error: any): {
isNetworkError: boolean
canRetry: boolean
shouldWaitForOnline: boolean
userMessage: string
retryDelay: number
} {
// ネットワークエラーの種類を判定
if (error instanceof TypeError && error.message.includes('fetch')) {
return {
isNetworkError: true,
canRetry: true,
shouldWaitForOnline: !this.isOnline,
userMessage: this.isOnline
? 'サーバーとの通信に失敗しました。しばらく待ってから再試行してください。'
: 'インターネット接続が切断されています。接続を確認してください。',
retryDelay: this.isOnline ? 2000 : 0
}
}
if (error.name === 'AbortError') {
return {
isNetworkError: true,
canRetry: true,
shouldWaitForOnline: false,
userMessage: 'リクエストがタイムアウトしました。再試行してください。',
retryDelay: 1000
}
}
if (error.status) {
const statusErrorMap: Record<number, any> = {
408: { // Request Timeout
canRetry: true,
shouldWaitForOnline: false,
userMessage: 'リクエストがタイムアウトしました。再試行してください。',
retryDelay: 2000
},
429: { // Too Many Requests
canRetry: true,
shouldWaitForOnline: false,
userMessage: 'リクエストが多すぎます。しばらく待ってから再試行してください。',
retryDelay: 10000
},
500: { // Internal Server Error
canRetry: true,
shouldWaitForOnline: false,
userMessage: 'サーバーエラーが発生しました。しばらく待ってから再試行してください。',
retryDelay: 5000
},
502: { // Bad Gateway
canRetry: true,
shouldWaitForOnline: false,
userMessage: 'サーバーが一時的に利用できません。しばらく待ってから再試行してください。',
retryDelay: 3000
},
503: { // Service Unavailable
canRetry: true,
shouldWaitForOnline: false,
userMessage: 'サービスが一時的に利用できません。しばらく待ってから再試行してください。',
retryDelay: 5000
}
}
const statusInfo = statusErrorMap[error.status]
if (statusInfo) {
return {
isNetworkError: true,
...statusInfo
}
}
}
return {
isNetworkError: false,
canRetry: false,
shouldWaitForOnline: false,
userMessage: '不明なエラーが発生しました。',
retryDelay: 0
}
}
// 自動リトライ機能付きFetch
static async fetchWithRetry(
url: string,
options: RequestInit = {},
maxRetries: number = 3,
baseDelay: number = 1000
): Promise<Response> {
let lastError: any
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
// オンライン状態になるまで待機
if (!this.isOnline) {
await this.waitForOnline()
}
// タイムアウト設定
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), 10000) // 10秒
const response = await fetch(url, {
...options,
signal: controller.signal
})
clearTimeout(timeout)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
return response
} catch (error) {
lastError = error
const errorInfo = this.handleNetworkError(error)
if (!errorInfo.canRetry || attempt === maxRetries) {
throw error
}
if (errorInfo.shouldWaitForOnline) {
await this.waitForOnline()
} else {
const delay = Math.min(baseDelay * Math.pow(2, attempt - 1), 30000)
await new Promise(resolve => setTimeout(resolve, delay))
}
console.log(`Network request attempt ${attempt} failed, retrying...`)
}
}
throw lastError
}
private static waitForOnline(): Promise<void> {
if (this.isOnline) {
return Promise.resolve()
}
return new Promise(resolve => {
const onlineHandler = () => {
resolve()
window.removeEventListener('online', onlineHandler)
}
window.addEventListener('online', onlineHandler)
})
}
static onOnline(callback: () => void): () => void {
this.onlineListeners.push(callback)
return () => {
const index = this.onlineListeners.indexOf(callback)
if (index > -1) {
this.onlineListeners.splice(index, 1)
}
}
}
static onOffline(callback: () => void): () => void {
this.offlineListeners.push(callback)
return () => {
const index = this.offlineListeners.indexOf(callback)
if (index > -1) {
this.offlineListeners.splice(index, 1)
}
}
}
}
ユーザーフレンドリーエラー表示
エラー表示コンポーネント
import React, { useState, useEffect } from 'react'
import { AppError, ErrorSeverity } from './ErrorHandler'
interface ErrorDisplayProps {
error: AppError
onRetry?: () => void
onDismiss?: () => void
showDetails?: boolean
}
export const ErrorDisplay: React.FC<ErrorDisplayProps> = ({
error,
onRetry,
onDismiss,
showDetails = false
}) => {
const [showTechnicalDetails, setShowTechnicalDetails] = useState(false)
const getSeverityStyles = (severity: ErrorSeverity) => {
const baseStyles = "p-4 rounded-lg border-l-4 mb-4"
switch (severity) {
case ErrorSeverity.LOW:
return `${baseStyles} bg-blue-50 border-blue-400 text-blue-700`
case ErrorSeverity.MEDIUM:
return `${baseStyles} bg-yellow-50 border-yellow-400 text-yellow-700`
case ErrorSeverity.HIGH:
return `${baseStyles} bg-orange-50 border-orange-400 text-orange-700`
case ErrorSeverity.CRITICAL:
return `${baseStyles} bg-red-50 border-red-400 text-red-700`
default:
return `${baseStyles} bg-gray-50 border-gray-400 text-gray-700`
}
}
const getSeverityIcon = (severity: ErrorSeverity) => {
switch (severity) {
case ErrorSeverity.LOW:
return "ℹ️"
case ErrorSeverity.MEDIUM:
return "⚠️"
case ErrorSeverity.HIGH:
return "❌"
case ErrorSeverity.CRITICAL:
return "🚨"
default:
return "❓"
}
}
return (
<div className={getSeverityStyles(error.severity)}>
<div className="flex items-start">
<span className="text-2xl mr-3">{getSeverityIcon(error.severity)}</span>
<div className="flex-1">
<h3 className="font-medium mb-2">
{error.userMessage}
</h3>
{error.recoverable && onRetry && (
<button
onClick={onRetry}
className="inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 mr-2"
>
再試行
</button>
)}
{onDismiss && (
<button
onClick={onDismiss}
className="inline-flex items-center px-3 py-1 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 mr-2"
>
閉じる
</button>
)}
{showDetails && (
<button
onClick={() => setShowTechnicalDetails(!showTechnicalDetails)}
className="inline-flex items-center text-sm text-gray-500 hover:text-gray-700"
>
{showTechnicalDetails ? '詳細を隠す' : '詳細を表示'}
</button>
)}
{showTechnicalDetails && (
<div className="mt-3 p-3 bg-gray-100 rounded text-sm font-mono">
<div><strong>エラーID:</strong> {error.id}</div>
<div><strong>カテゴリ:</strong> {error.category}</div>
<div><strong>コード:</strong> {error.code}</div>
<div><strong>時刻:</strong> {error.timestamp.toLocaleString()}</div>
{error.context && (
<div><strong>コンテキスト:</strong> {JSON.stringify(error.context, null, 2)}</div>
)}
</div>
)}
</div>
</div>
</div>
)
}
// エラートースト通知
export const useErrorToast = () => {
const [errors, setErrors] = useState<AppError[]>([])
const showError = (error: AppError) => {
setErrors(prev => [...prev, error])
// 自動削除(重要度に応じて表示時間を調整)
const autoRemoveDelay = error.severity === ErrorSeverity.CRITICAL ? 10000 : 5000
setTimeout(() => {
removeError(error.id)
}, autoRemoveDelay)
}
const removeError = (errorId: string) => {
setErrors(prev => prev.filter(error => error.id !== errorId))
}
const ErrorToastContainer = () => (
<div className="fixed top-4 right-4 z-50 space-y-2">
{errors.map(error => (
<ErrorDisplay
key={error.id}
error={error}
onDismiss={() => removeError(error.id)}
/>
))}
</div>
)
return {
showError,
removeError,
ErrorToastContainer
}
}
このエラーハンドリングガイドにより、Supabaseアプリケーションで発生する様々なエラーを適切に処理し、ユーザーに分かりやすいメッセージを提供できるようになります。