monday_ruby 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.env +1 -1
- data/.rspec +0 -1
- data/.rubocop.yml +19 -0
- data/.simplecov +1 -0
- data/CHANGELOG.md +49 -0
- data/CONTRIBUTING.md +165 -0
- data/README.md +167 -88
- data/docs/.vitepress/config.mjs +255 -0
- data/docs/.vitepress/theme/index.js +4 -0
- data/docs/.vitepress/theme/style.css +43 -0
- data/docs/README.md +80 -0
- data/docs/explanation/architecture.md +507 -0
- data/docs/explanation/best-practices/errors.md +478 -0
- data/docs/explanation/best-practices/performance.md +1084 -0
- data/docs/explanation/best-practices/rate-limiting.md +630 -0
- data/docs/explanation/best-practices/testing.md +820 -0
- data/docs/explanation/column-values.md +857 -0
- data/docs/explanation/design.md +795 -0
- data/docs/explanation/graphql.md +356 -0
- data/docs/explanation/migration/v1.md +808 -0
- data/docs/explanation/pagination.md +447 -0
- data/docs/guides/advanced/batch.md +1274 -0
- data/docs/guides/advanced/complex-queries.md +1114 -0
- data/docs/guides/advanced/errors.md +818 -0
- data/docs/guides/advanced/pagination.md +934 -0
- data/docs/guides/advanced/rate-limiting.md +981 -0
- data/docs/guides/authentication.md +286 -0
- data/docs/guides/boards/create.md +386 -0
- data/docs/guides/boards/delete.md +405 -0
- data/docs/guides/boards/duplicate.md +511 -0
- data/docs/guides/boards/query.md +530 -0
- data/docs/guides/boards/update.md +453 -0
- data/docs/guides/columns/create.md +452 -0
- data/docs/guides/columns/metadata.md +492 -0
- data/docs/guides/columns/query.md +455 -0
- data/docs/guides/columns/update-multiple.md +459 -0
- data/docs/guides/columns/update-values.md +509 -0
- data/docs/guides/files/add-to-column.md +40 -0
- data/docs/guides/files/add-to-update.md +37 -0
- data/docs/guides/files/clear-column.md +33 -0
- data/docs/guides/first-request.md +285 -0
- data/docs/guides/folders/manage.md +750 -0
- data/docs/guides/groups/items.md +626 -0
- data/docs/guides/groups/manage.md +501 -0
- data/docs/guides/installation.md +169 -0
- data/docs/guides/items/create.md +493 -0
- data/docs/guides/items/delete.md +514 -0
- data/docs/guides/items/query.md +605 -0
- data/docs/guides/items/subitems.md +483 -0
- data/docs/guides/items/update.md +699 -0
- data/docs/guides/updates/manage.md +619 -0
- data/docs/guides/use-cases/dashboard.md +1421 -0
- data/docs/guides/use-cases/import.md +1962 -0
- data/docs/guides/use-cases/task-management.md +1381 -0
- data/docs/guides/workspaces/manage.md +502 -0
- data/docs/index.md +69 -0
- data/docs/package-lock.json +2468 -0
- data/docs/package.json +13 -0
- data/docs/reference/client.md +540 -0
- data/docs/reference/configuration.md +586 -0
- data/docs/reference/errors.md +693 -0
- data/docs/reference/resources/account.md +208 -0
- data/docs/reference/resources/activity-log.md +369 -0
- data/docs/reference/resources/board-view.md +359 -0
- data/docs/reference/resources/board.md +393 -0
- data/docs/reference/resources/column.md +543 -0
- data/docs/reference/resources/file.md +236 -0
- data/docs/reference/resources/folder.md +386 -0
- data/docs/reference/resources/group.md +507 -0
- data/docs/reference/resources/item.md +348 -0
- data/docs/reference/resources/subitem.md +267 -0
- data/docs/reference/resources/update.md +259 -0
- data/docs/reference/resources/workspace.md +213 -0
- data/docs/reference/response.md +560 -0
- data/docs/tutorial/first-integration.md +713 -0
- data/lib/monday/client.rb +41 -2
- data/lib/monday/configuration.rb +13 -0
- data/lib/monday/deprecation.rb +23 -0
- data/lib/monday/error.rb +5 -2
- data/lib/monday/request.rb +19 -1
- data/lib/monday/resources/base.rb +4 -0
- data/lib/monday/resources/board.rb +52 -0
- data/lib/monday/resources/column.rb +6 -0
- data/lib/monday/resources/file.rb +56 -0
- data/lib/monday/resources/folder.rb +55 -0
- data/lib/monday/resources/group.rb +66 -0
- data/lib/monday/resources/item.rb +62 -0
- data/lib/monday/util.rb +33 -1
- data/lib/monday/version.rb +1 -1
- data/lib/monday_ruby.rb +1 -0
- metadata +92 -11
- data/monday_ruby.gemspec +0 -39
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
# Error Handling Best Practices
|
|
2
|
+
|
|
3
|
+
Error handling is a critical aspect of building resilient applications that integrate with external APIs. This guide explores the philosophy and patterns for effective error handling when using the monday_ruby gem.
|
|
4
|
+
|
|
5
|
+
## Error Handling Philosophy
|
|
6
|
+
|
|
7
|
+
In Ruby, exceptions are the primary mechanism for handling error conditions. The monday_ruby gem embraces this philosophy by providing a rich exception hierarchy that maps to specific API error conditions. This approach follows the principle that **errors should be specific enough to make informed decisions about recovery, but not so granular that they become burdensome to handle**.
|
|
8
|
+
|
|
9
|
+
The alternative—using status codes or error objects—would require checking return values after every API call, leading to verbose, error-prone code. Ruby's exception model allows your "happy path" code to remain clean while still providing robust error handling when needed.
|
|
10
|
+
|
|
11
|
+
## Why Specific Exception Types Matter
|
|
12
|
+
|
|
13
|
+
The monday_ruby gem provides specific exception classes for different error conditions:
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
Monday::AuthorizationError # Authentication/authorization failures
|
|
17
|
+
Monday::InvalidRequestError # Malformed requests
|
|
18
|
+
Monday::ResourceNotFoundError # Missing resources
|
|
19
|
+
Monday::ComplexityError # Query complexity exceeded
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
**Why not just catch a generic `Monday::Error`?** Specific exceptions enable different recovery strategies:
|
|
23
|
+
|
|
24
|
+
- **AuthorizationError**: Might indicate expired credentials → refresh token or prompt re-authentication
|
|
25
|
+
- **ComplexityError**: Query too complex → simplify query or retry with pagination
|
|
26
|
+
- **ResourceNotFoundError**: Item deleted → remove from local cache or skip processing
|
|
27
|
+
- **InvalidRequestError**: Programming error → log for debugging, don't retry
|
|
28
|
+
|
|
29
|
+
Using specific exceptions makes your intent clear and prevents accidentally catching unrelated errors.
|
|
30
|
+
|
|
31
|
+
## When to Rescue Specific Errors vs Base Classes
|
|
32
|
+
|
|
33
|
+
The decision of which exception level to catch depends on your recovery strategy:
|
|
34
|
+
|
|
35
|
+
### Catch Specific Exceptions When:
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
begin
|
|
39
|
+
client.item.query(ids: [item_id])
|
|
40
|
+
rescue Monday::ResourceNotFoundError
|
|
41
|
+
# Specific recovery: remove from local database
|
|
42
|
+
Item.find_by(monday_id: item_id)&.destroy
|
|
43
|
+
rescue Monday::AuthorizationError
|
|
44
|
+
# Specific recovery: refresh credentials
|
|
45
|
+
refresh_monday_token
|
|
46
|
+
retry
|
|
47
|
+
end
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
This approach is best when **different errors require different recovery actions**.
|
|
51
|
+
|
|
52
|
+
### Catch Base Exception When:
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
begin
|
|
56
|
+
client.board.query(ids: [board_id])
|
|
57
|
+
rescue Monday::Error => e
|
|
58
|
+
# Generic recovery: log and notify
|
|
59
|
+
logger.error("Monday API error: #{e.message}")
|
|
60
|
+
notify_monitoring_system(e)
|
|
61
|
+
nil # Return nil to allow graceful degradation
|
|
62
|
+
end
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
This approach is best when **all API errors should be handled the same way** (logging, monitoring, graceful failure).
|
|
66
|
+
|
|
67
|
+
### Catch at Multiple Levels:
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
begin
|
|
71
|
+
sync_monday_data
|
|
72
|
+
rescue Monday::ComplexityError
|
|
73
|
+
# Specific: wait and retry
|
|
74
|
+
sleep(60)
|
|
75
|
+
retry
|
|
76
|
+
rescue Monday::Error => e
|
|
77
|
+
# Catch-all: log and fail gracefully
|
|
78
|
+
logger.error("Sync failed: #{e.message}")
|
|
79
|
+
false
|
|
80
|
+
end
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
This pattern handles specific cases specially while catching all other API errors generically.
|
|
84
|
+
|
|
85
|
+
## Error Recovery Patterns
|
|
86
|
+
|
|
87
|
+
### 1. Immediate Retry (Transient Errors)
|
|
88
|
+
|
|
89
|
+
Some errors are transient—temporary network issues, service hiccups. These warrant immediate retry:
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
def fetch_with_retry(max_attempts: 3)
|
|
93
|
+
attempts = 0
|
|
94
|
+
begin
|
|
95
|
+
attempts += 1
|
|
96
|
+
client.board.query(ids: [board_id])
|
|
97
|
+
rescue Monday::InternalServerError, Monday::ServiceUnavailableError
|
|
98
|
+
retry if attempts < max_attempts
|
|
99
|
+
raise
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**When to use**: Network timeouts, 5xx errors, temporary service issues.
|
|
105
|
+
|
|
106
|
+
**Trade-off**: Immediate retries can compound problems during outages. Use sparingly.
|
|
107
|
+
|
|
108
|
+
### 2. Exponential Backoff (Rate Limiting)
|
|
109
|
+
|
|
110
|
+
Rate limit errors should be retried with increasing delays:
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
def fetch_with_backoff(max_attempts: 5)
|
|
114
|
+
attempts = 0
|
|
115
|
+
begin
|
|
116
|
+
attempts += 1
|
|
117
|
+
client.item.query(ids: item_ids)
|
|
118
|
+
rescue Monday::ComplexityError => e
|
|
119
|
+
wait_time = 2 ** attempts # 2, 4, 8, 16, 32 seconds
|
|
120
|
+
sleep(wait_time)
|
|
121
|
+
retry if attempts < max_attempts
|
|
122
|
+
raise
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**Why exponential**: Linear backoff (1s, 2s, 3s) doesn't reduce load enough. Exponential backoff gives the service time to recover.
|
|
128
|
+
|
|
129
|
+
**Trade-off**: Long waits can impact user experience. Consider background processing for retries.
|
|
130
|
+
|
|
131
|
+
### 3. Circuit Breaker (Cascading Failures)
|
|
132
|
+
|
|
133
|
+
When an API is consistently failing, stop making requests to prevent cascading failures:
|
|
134
|
+
|
|
135
|
+
```ruby
|
|
136
|
+
class MondayCircuitBreaker
|
|
137
|
+
def initialize(failure_threshold: 5, timeout: 60)
|
|
138
|
+
@failure_count = 0
|
|
139
|
+
@failure_threshold = failure_threshold
|
|
140
|
+
@timeout = timeout
|
|
141
|
+
@opened_at = nil
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def call
|
|
145
|
+
raise CircuitOpenError if circuit_open?
|
|
146
|
+
|
|
147
|
+
begin
|
|
148
|
+
result = yield
|
|
149
|
+
reset_failures
|
|
150
|
+
result
|
|
151
|
+
rescue Monday::Error => e
|
|
152
|
+
record_failure
|
|
153
|
+
raise
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
private
|
|
158
|
+
|
|
159
|
+
def circuit_open?
|
|
160
|
+
@failure_count >= @failure_threshold &&
|
|
161
|
+
(@opened_at.nil? || Time.now - @opened_at < @timeout)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def record_failure
|
|
165
|
+
@failure_count += 1
|
|
166
|
+
@opened_at = Time.now if @failure_count == @failure_threshold
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def reset_failures
|
|
170
|
+
@failure_count = 0
|
|
171
|
+
@opened_at = nil
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**When to use**: High-traffic applications where API failures could cascade to other systems.
|
|
177
|
+
|
|
178
|
+
**Trade-off**: Adds complexity. May reject requests even when service has recovered.
|
|
179
|
+
|
|
180
|
+
### 4. Graceful Degradation
|
|
181
|
+
|
|
182
|
+
Instead of failing completely, provide reduced functionality:
|
|
183
|
+
|
|
184
|
+
```ruby
|
|
185
|
+
def get_board_data(board_id)
|
|
186
|
+
begin
|
|
187
|
+
client.board.query(ids: [board_id])
|
|
188
|
+
rescue Monday::Error => e
|
|
189
|
+
logger.warn("Failed to fetch live data: #{e.message}")
|
|
190
|
+
# Fall back to cached data
|
|
191
|
+
Rails.cache.read("board_#{board_id}")
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
**When to use**: User-facing features where some data is better than no data.
|
|
197
|
+
|
|
198
|
+
**Trade-off**: Users get stale data. Must communicate data freshness clearly.
|
|
199
|
+
|
|
200
|
+
## Retry Strategies: When and How Many Times
|
|
201
|
+
|
|
202
|
+
**How many retries?**
|
|
203
|
+
- **Transient errors**: 2-3 retries (network blips are usually brief)
|
|
204
|
+
- **Rate limiting**: 5-7 retries (may need multiple backoff intervals)
|
|
205
|
+
- **Service outages**: 0-1 retries (unlikely to resolve quickly)
|
|
206
|
+
|
|
207
|
+
**When not to retry:**
|
|
208
|
+
- `InvalidRequestError`: The request is malformed; retrying won't help
|
|
209
|
+
- `AuthorizationError`: Credentials are invalid; retry only after refreshing
|
|
210
|
+
- `ResourceNotFoundError`: The resource doesn't exist; retrying won't create it
|
|
211
|
+
|
|
212
|
+
**Retry budgets**: Consider a total time budget rather than retry count:
|
|
213
|
+
|
|
214
|
+
```ruby
|
|
215
|
+
def fetch_with_budget(timeout: 30)
|
|
216
|
+
deadline = Time.now + timeout
|
|
217
|
+
attempts = 0
|
|
218
|
+
|
|
219
|
+
begin
|
|
220
|
+
attempts += 1
|
|
221
|
+
client.board.query(ids: [board_id])
|
|
222
|
+
rescue Monday::ComplexityError
|
|
223
|
+
wait_time = 2 ** attempts
|
|
224
|
+
raise if Time.now + wait_time > deadline
|
|
225
|
+
sleep(wait_time)
|
|
226
|
+
retry
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## Logging Errors for Debugging and Monitoring
|
|
232
|
+
|
|
233
|
+
Effective logging balances detail with signal-to-noise ratio.
|
|
234
|
+
|
|
235
|
+
### What to Log
|
|
236
|
+
|
|
237
|
+
**Always log:**
|
|
238
|
+
- Exception class and message
|
|
239
|
+
- Request details (endpoint, parameters—except secrets)
|
|
240
|
+
- Context (user ID, board ID, operation being performed)
|
|
241
|
+
- Timestamp and correlation ID
|
|
242
|
+
|
|
243
|
+
```ruby
|
|
244
|
+
begin
|
|
245
|
+
client.item.create(board_id: board_id, item_name: name)
|
|
246
|
+
rescue Monday::Error => e
|
|
247
|
+
logger.error({
|
|
248
|
+
error_class: e.class.name,
|
|
249
|
+
error_message: e.message,
|
|
250
|
+
operation: 'create_item',
|
|
251
|
+
board_id: board_id,
|
|
252
|
+
item_name: name,
|
|
253
|
+
user_id: current_user.id,
|
|
254
|
+
correlation_id: request_id
|
|
255
|
+
}.to_json)
|
|
256
|
+
raise
|
|
257
|
+
end
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
**Don't log:**
|
|
261
|
+
- API tokens or credentials
|
|
262
|
+
- Sensitive user data (unless required for compliance)
|
|
263
|
+
- Full response bodies (unless debugging a specific issue)
|
|
264
|
+
|
|
265
|
+
### Log Levels
|
|
266
|
+
|
|
267
|
+
- **ERROR**: Unexpected failures that impact functionality
|
|
268
|
+
- **WARN**: Handled errors (graceful degradation, retries that succeed)
|
|
269
|
+
- **INFO**: Normal API errors that are part of business logic (e.g., validation failures)
|
|
270
|
+
- **DEBUG**: Full request/response details (disable in production)
|
|
271
|
+
|
|
272
|
+
```ruby
|
|
273
|
+
rescue Monday::ResourceNotFoundError => e
|
|
274
|
+
logger.info("Item not found, skipping: #{item_id}") # Expected condition
|
|
275
|
+
nil
|
|
276
|
+
rescue Monday::InternalServerError => e
|
|
277
|
+
logger.error("Monday API error: #{e.message}") # Unexpected failure
|
|
278
|
+
raise
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
## User-Facing vs Internal Error Messages
|
|
282
|
+
|
|
283
|
+
Exception messages serve two audiences:
|
|
284
|
+
|
|
285
|
+
### Internal Messages (for developers/logs)
|
|
286
|
+
|
|
287
|
+
Include technical details:
|
|
288
|
+
```ruby
|
|
289
|
+
"Failed to create item: ComplexityError - Query complexity exceeds limit (60/58).
|
|
290
|
+
Reduce query fields or implement pagination."
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### User-Facing Messages (for end users)
|
|
294
|
+
|
|
295
|
+
Be generic and actionable:
|
|
296
|
+
```ruby
|
|
297
|
+
begin
|
|
298
|
+
client.item.create(...)
|
|
299
|
+
rescue Monday::ComplexityError
|
|
300
|
+
flash[:error] = "The operation is too complex. Please try creating fewer items at once."
|
|
301
|
+
rescue Monday::Error
|
|
302
|
+
flash[:error] = "We couldn't complete this action. Please try again later."
|
|
303
|
+
end
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
**Why separate them?**
|
|
307
|
+
- Users don't need technical details
|
|
308
|
+
- Exposing internal errors can be a security risk
|
|
309
|
+
- User messages should suggest solutions, not explain implementation
|
|
310
|
+
|
|
311
|
+
## Graceful Degradation Patterns
|
|
312
|
+
|
|
313
|
+
Graceful degradation means providing partial functionality when full functionality fails.
|
|
314
|
+
|
|
315
|
+
### Pattern 1: Cache Fallback
|
|
316
|
+
|
|
317
|
+
```ruby
|
|
318
|
+
def get_board_items(board_id)
|
|
319
|
+
begin
|
|
320
|
+
items = client.item.query_by_board(board_id: board_id)
|
|
321
|
+
Rails.cache.write("board_items_#{board_id}", items, expires_in: 1.hour)
|
|
322
|
+
items
|
|
323
|
+
rescue Monday::Error => e
|
|
324
|
+
logger.warn("API failed, using cache: #{e.message}")
|
|
325
|
+
Rails.cache.read("board_items_#{board_id}") || []
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### Pattern 2: Feature Toggle
|
|
331
|
+
|
|
332
|
+
```ruby
|
|
333
|
+
def sync_monday_data
|
|
334
|
+
client.board.query(...)
|
|
335
|
+
rescue Monday::Error => e
|
|
336
|
+
logger.error("Sync failed: #{e.message}")
|
|
337
|
+
disable_monday_sync_feature!
|
|
338
|
+
notify_admins
|
|
339
|
+
end
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### Pattern 3: Partial Success
|
|
343
|
+
|
|
344
|
+
```ruby
|
|
345
|
+
def sync_multiple_boards(board_ids)
|
|
346
|
+
results = { success: [], failed: [] }
|
|
347
|
+
|
|
348
|
+
board_ids.each do |board_id|
|
|
349
|
+
begin
|
|
350
|
+
sync_board(board_id)
|
|
351
|
+
results[:success] << board_id
|
|
352
|
+
rescue Monday::Error => e
|
|
353
|
+
logger.error("Board #{board_id} sync failed: #{e.message}")
|
|
354
|
+
results[:failed] << board_id
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
results
|
|
359
|
+
end
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
**Trade-offs**: Users may not notice degraded functionality, leading to confusion. Always communicate what's working and what's not.
|
|
363
|
+
|
|
364
|
+
## Transaction-Like Error Handling
|
|
365
|
+
|
|
366
|
+
APIs don't support transactions like databases, but you can implement transaction-like patterns:
|
|
367
|
+
|
|
368
|
+
### Pattern 1: Compensation (Undo on Error)
|
|
369
|
+
|
|
370
|
+
```ruby
|
|
371
|
+
def create_board_with_items(board_name, items)
|
|
372
|
+
board = nil
|
|
373
|
+
|
|
374
|
+
begin
|
|
375
|
+
board = client.board.create(board_name: board_name).dig('data', 'create_board')
|
|
376
|
+
|
|
377
|
+
items.each do |item|
|
|
378
|
+
client.item.create(board_id: board['id'], item_name: item['name'])
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
board
|
|
382
|
+
rescue Monday::Error => e
|
|
383
|
+
# Compensate: delete the board if item creation failed
|
|
384
|
+
client.board.delete(board_id: board['id']) if board
|
|
385
|
+
raise
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### Pattern 2: All-or-Nothing (Validate First)
|
|
391
|
+
|
|
392
|
+
```ruby
|
|
393
|
+
def bulk_update_items(updates)
|
|
394
|
+
# Validate all updates first
|
|
395
|
+
updates.each do |update|
|
|
396
|
+
validate_update!(update)
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
# If validation passes, perform updates
|
|
400
|
+
updates.map do |update|
|
|
401
|
+
client.item.update(item_id: update[:id], column_values: update[:values])
|
|
402
|
+
end
|
|
403
|
+
rescue Monday::Error => e
|
|
404
|
+
logger.error("Bulk update failed, no changes made: #{e.message}")
|
|
405
|
+
raise
|
|
406
|
+
end
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
**Limitation**: Between validation and execution, state can change. True atomicity isn't possible with APIs.
|
|
410
|
+
|
|
411
|
+
## Testing Error Scenarios
|
|
412
|
+
|
|
413
|
+
Error handling code is only as good as its tests.
|
|
414
|
+
|
|
415
|
+
### Test Each Error Type
|
|
416
|
+
|
|
417
|
+
```ruby
|
|
418
|
+
RSpec.describe 'error handling' do
|
|
419
|
+
it 'retries on complexity errors' do
|
|
420
|
+
allow(client).to receive(:make_request)
|
|
421
|
+
.and_raise(Monday::ComplexityError)
|
|
422
|
+
.exactly(3).times
|
|
423
|
+
.and_return(mock_response)
|
|
424
|
+
|
|
425
|
+
result = fetch_with_retry
|
|
426
|
+
expect(result).to eq(mock_response)
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
it 'does not retry on invalid request errors' do
|
|
430
|
+
allow(client).to receive(:make_request)
|
|
431
|
+
.and_raise(Monday::InvalidRequestError)
|
|
432
|
+
|
|
433
|
+
expect { fetch_with_retry }.to raise_error(Monday::InvalidRequestError)
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
### Test Retry Logic
|
|
439
|
+
|
|
440
|
+
```ruby
|
|
441
|
+
it 'implements exponential backoff' do
|
|
442
|
+
allow(client).to receive(:make_request)
|
|
443
|
+
.and_raise(Monday::ComplexityError)
|
|
444
|
+
|
|
445
|
+
expect(self).to receive(:sleep).with(2)
|
|
446
|
+
expect(self).to receive(:sleep).with(4)
|
|
447
|
+
expect(self).to receive(:sleep).with(8)
|
|
448
|
+
|
|
449
|
+
expect { fetch_with_backoff(max_attempts: 4) }
|
|
450
|
+
.to raise_error(Monday::ComplexityError)
|
|
451
|
+
end
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
### Test Graceful Degradation
|
|
455
|
+
|
|
456
|
+
```ruby
|
|
457
|
+
it 'falls back to cache on error' do
|
|
458
|
+
allow(client).to receive(:make_request)
|
|
459
|
+
.and_raise(Monday::InternalServerError)
|
|
460
|
+
|
|
461
|
+
Rails.cache.write('board_123', cached_data)
|
|
462
|
+
|
|
463
|
+
result = get_board_data(123)
|
|
464
|
+
expect(result).to eq(cached_data)
|
|
465
|
+
end
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
## Key Takeaways
|
|
469
|
+
|
|
470
|
+
1. **Be specific**: Use specific exception types to enable targeted recovery
|
|
471
|
+
2. **Retry wisely**: Use exponential backoff for rate limits, limited retries for transient errors
|
|
472
|
+
3. **Fail gracefully**: Provide reduced functionality when possible
|
|
473
|
+
4. **Log thoughtfully**: Include context for debugging, but protect sensitive data
|
|
474
|
+
5. **Separate concerns**: Internal errors ≠ user-facing messages
|
|
475
|
+
6. **Test thoroughly**: Error handling code needs tests too
|
|
476
|
+
7. **Monitor**: Track error rates to identify patterns and systemic issues
|
|
477
|
+
|
|
478
|
+
Error handling is not just about preventing crashes—it's about creating resilient systems that degrade gracefully and recover automatically when possible.
|