TypeScript · 6724 bytes Raw Blame History
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 }