| 1 |
import { ref } from 'vue' |
| 2 |
|
| 3 |
export interface ErrorInfo { |
| 4 |
id: string |
| 5 |
type: 'network' | 'validation' | 'auth' | 'server' | 'client' | 'unknown' |
| 6 |
message: string |
| 7 |
details?: string |
| 8 |
timestamp: Date |
| 9 |
retryable: boolean |
| 10 |
context?: Record<string, any> |
| 11 |
} |
| 12 |
|
| 13 |
const errors = ref<ErrorInfo[]>([]) |
| 14 |
|
| 15 |
export function useErrorHandler() { |
| 16 |
function handleError(error: any, context?: Record<string, any>): ErrorInfo { |
| 17 |
const errorInfo = parseError(error, context) |
| 18 |
|
| 19 |
// Store error for debugging |
| 20 |
errors.value.unshift(errorInfo) |
| 21 |
|
| 22 |
// Keep only last 50 errors |
| 23 |
if (errors.value.length > 50) { |
| 24 |
errors.value.splice(50) |
| 25 |
} |
| 26 |
|
| 27 |
// Show user notification |
| 28 |
showErrorNotification(errorInfo) |
| 29 |
|
| 30 |
// Log error for development |
| 31 |
if (import.meta.env.DEV) { |
| 32 |
console.error('Error handled:', errorInfo, error) |
| 33 |
} |
| 34 |
|
| 35 |
return errorInfo |
| 36 |
} |
| 37 |
|
| 38 |
function parseError(error: any, context?: Record<string, any>): ErrorInfo { |
| 39 |
const id = crypto.randomUUID() |
| 40 |
const timestamp = new Date() |
| 41 |
|
| 42 |
// Network errors |
| 43 |
if (error.name === 'NetworkError' || error.code === 'NETWORK_ERROR') { |
| 44 |
return { |
| 45 |
id, |
| 46 |
type: 'network', |
| 47 |
message: 'Network connection failed', |
| 48 |
details: 'Please check your internet connection and try again.', |
| 49 |
timestamp, |
| 50 |
retryable: true, |
| 51 |
context, |
| 52 |
} |
| 53 |
} |
| 54 |
|
| 55 |
// Fetch/Axios errors |
| 56 |
if (error.response) { |
| 57 |
const status = error.response.status |
| 58 |
const data = error.response.data |
| 59 |
|
| 60 |
if (status === 401) { |
| 61 |
return { |
| 62 |
id, |
| 63 |
type: 'auth', |
| 64 |
message: 'Authentication required', |
| 65 |
details: 'Please log in again to continue.', |
| 66 |
timestamp, |
| 67 |
retryable: false, |
| 68 |
context, |
| 69 |
} |
| 70 |
} |
| 71 |
|
| 72 |
if (status === 403) { |
| 73 |
return { |
| 74 |
id, |
| 75 |
type: 'auth', |
| 76 |
message: 'Access denied', |
| 77 |
details: 'You do not have permission to perform this action.', |
| 78 |
timestamp, |
| 79 |
retryable: false, |
| 80 |
context, |
| 81 |
} |
| 82 |
} |
| 83 |
|
| 84 |
if (status === 404) { |
| 85 |
return { |
| 86 |
id, |
| 87 |
type: 'client', |
| 88 |
message: 'Resource not found', |
| 89 |
details: 'The requested resource could not be found.', |
| 90 |
timestamp, |
| 91 |
retryable: false, |
| 92 |
context, |
| 93 |
} |
| 94 |
} |
| 95 |
|
| 96 |
if (status === 413) { |
| 97 |
return { |
| 98 |
id, |
| 99 |
type: 'validation', |
| 100 |
message: 'File too large', |
| 101 |
details: 'The file you are trying to upload exceeds the maximum size limit.', |
| 102 |
timestamp, |
| 103 |
retryable: false, |
| 104 |
context, |
| 105 |
} |
| 106 |
} |
| 107 |
|
| 108 |
if (status === 422) { |
| 109 |
return { |
| 110 |
id, |
| 111 |
type: 'validation', |
| 112 |
message: 'Invalid data', |
| 113 |
details: data?.message || 'Please check your input and try again.', |
| 114 |
timestamp, |
| 115 |
retryable: false, |
| 116 |
context, |
| 117 |
} |
| 118 |
} |
| 119 |
|
| 120 |
if (status === 429) { |
| 121 |
return { |
| 122 |
id, |
| 123 |
type: 'client', |
| 124 |
message: 'Too many requests', |
| 125 |
details: 'Please wait a moment before trying again.', |
| 126 |
timestamp, |
| 127 |
retryable: true, |
| 128 |
context, |
| 129 |
} |
| 130 |
} |
| 131 |
|
| 132 |
if (status >= 500) { |
| 133 |
return { |
| 134 |
id, |
| 135 |
type: 'server', |
| 136 |
message: 'Server error', |
| 137 |
details: 'Something went wrong on our end. Please try again later.', |
| 138 |
timestamp, |
| 139 |
retryable: true, |
| 140 |
context, |
| 141 |
} |
| 142 |
} |
| 143 |
|
| 144 |
return { |
| 145 |
id, |
| 146 |
type: 'unknown', |
| 147 |
message: data?.message || 'An unexpected error occurred', |
| 148 |
details: data?.details || `HTTP ${status} error`, |
| 149 |
timestamp, |
| 150 |
retryable: status >= 500, |
| 151 |
context, |
| 152 |
} |
| 153 |
} |
| 154 |
|
| 155 |
// Validation errors |
| 156 |
if (error.name === 'ValidationError' || error.type === 'validation') { |
| 157 |
return { |
| 158 |
id, |
| 159 |
type: 'validation', |
| 160 |
message: error.message || 'Validation failed', |
| 161 |
details: 'Please check your input and try again.', |
| 162 |
timestamp, |
| 163 |
retryable: false, |
| 164 |
context, |
| 165 |
} |
| 166 |
} |
| 167 |
|
| 168 |
// Timeout errors |
| 169 |
if (error.name === 'TimeoutError' || error.code === 'ECONNABORTED') { |
| 170 |
return { |
| 171 |
id, |
| 172 |
type: 'network', |
| 173 |
message: 'Request timeout', |
| 174 |
details: 'The request took too long to complete. Please try again.', |
| 175 |
timestamp, |
| 176 |
retryable: true, |
| 177 |
context, |
| 178 |
} |
| 179 |
} |
| 180 |
|
| 181 |
// Generic error fallback |
| 182 |
return { |
| 183 |
id, |
| 184 |
type: 'unknown', |
| 185 |
message: error.message || 'An unexpected error occurred', |
| 186 |
details: 'Please try again or contact support if the problem persists.', |
| 187 |
timestamp, |
| 188 |
retryable: true, |
| 189 |
context, |
| 190 |
} |
| 191 |
} |
| 192 |
|
| 193 |
function showErrorNotification(errorInfo: ErrorInfo) { |
| 194 |
if (!window.$notify) return |
| 195 |
|
| 196 |
const title = getErrorTitle(errorInfo.type) |
| 197 |
const duration = errorInfo.retryable ? 8000 : 5000 |
| 198 |
|
| 199 |
window.$notify.error(errorInfo.message, title, duration) |
| 200 |
} |
| 201 |
|
| 202 |
function getErrorTitle(type: ErrorInfo['type']): string { |
| 203 |
switch (type) { |
| 204 |
case 'network': |
| 205 |
return 'Connection Error' |
| 206 |
case 'auth': |
| 207 |
return 'Authentication Error' |
| 208 |
case 'validation': |
| 209 |
return 'Validation Error' |
| 210 |
case 'server': |
| 211 |
return 'Server Error' |
| 212 |
case 'client': |
| 213 |
return 'Client Error' |
| 214 |
default: |
| 215 |
return 'Error' |
| 216 |
} |
| 217 |
} |
| 218 |
|
| 219 |
function clearErrors() { |
| 220 |
errors.value = [] |
| 221 |
} |
| 222 |
|
| 223 |
function getRecentErrors(limit = 10) { |
| 224 |
return errors.value.slice(0, limit) |
| 225 |
} |
| 226 |
|
| 227 |
function retry(originalFunction: Function, errorInfo: ErrorInfo) { |
| 228 |
if (!errorInfo.retryable) { |
| 229 |
window.$notify?.warning('This action cannot be retried') |
| 230 |
return |
| 231 |
} |
| 232 |
|
| 233 |
try { |
| 234 |
return originalFunction() |
| 235 |
} catch (error) { |
| 236 |
handleError(error, { ...errorInfo.context, retry: true }) |
| 237 |
} |
| 238 |
} |
| 239 |
|
| 240 |
return { |
| 241 |
handleError, |
| 242 |
clearErrors, |
| 243 |
getRecentErrors, |
| 244 |
retry, |
| 245 |
errors: errors.value, |
| 246 |
} |
| 247 |
} |
| 248 |
|
| 249 |
// Global error handler for uncaught errors |
| 250 |
export function setupGlobalErrorHandler() { |
| 251 |
const { handleError } = useErrorHandler() |
| 252 |
|
| 253 |
// Handle uncaught JavaScript errors |
| 254 |
window.addEventListener('error', (event) => { |
| 255 |
handleError(event.error, { |
| 256 |
filename: event.filename, |
| 257 |
lineno: event.lineno, |
| 258 |
colno: event.colno, |
| 259 |
}) |
| 260 |
}) |
| 261 |
|
| 262 |
// Handle unhandled promise rejections |
| 263 |
window.addEventListener('unhandledrejection', (event) => { |
| 264 |
handleError(event.reason, { |
| 265 |
type: 'unhandled_promise_rejection', |
| 266 |
}) |
| 267 |
}) |
| 268 |
|
| 269 |
// Handle Vue errors (if using Vue) |
| 270 |
if (window.Vue) { |
| 271 |
window.Vue.config.errorHandler = (error: Error, instance: any, info: string) => { |
| 272 |
handleError(error, { |
| 273 |
component: instance?.$options.name || 'Unknown', |
| 274 |
info, |
| 275 |
}) |
| 276 |
} |
| 277 |
} |
| 278 |
} |