0
# Error Handling
1
2
Comprehensive error handling system with specific exception types for different API error conditions, HTTP errors, and timeout scenarios. The client provides detailed error information to help diagnose and handle issues appropriately.
3
4
## Capabilities
5
6
### Exception Hierarchy
7
8
#### Base HTTP Exceptions
9
10
```python { .api }
11
class RequestTimeoutError(Exception):
12
"""Exception for requests that timeout."""
13
14
code: str = "notionhq_client_request_timeout"
15
16
def __init__(self, message="Request to Notion API has timed out"):
17
"""
18
Initialize timeout error.
19
20
Parameters:
21
- message: str, error message (default provided)
22
"""
23
24
class HTTPResponseError(Exception):
25
"""Exception for HTTP errors."""
26
27
code: str = "notionhq_client_response_error"
28
status: int
29
headers: httpx.Headers
30
body: str
31
32
def __init__(self, response, message=None):
33
"""
34
Initialize HTTP response error.
35
36
Parameters:
37
- response: httpx.Response, the failed HTTP response
38
- message: str, optional custom error message
39
"""
40
```
41
42
#### API Response Exceptions
43
44
```python { .api }
45
class APIResponseError(HTTPResponseError):
46
"""An error raised by Notion API."""
47
48
code: APIErrorCode
49
50
def __init__(self, response, message, code):
51
"""
52
Initialize API response error.
53
54
Parameters:
55
- response: httpx.Response, the failed HTTP response
56
- message: str, error message from API
57
- code: APIErrorCode, specific error code from API
58
"""
59
```
60
61
### Error Codes
62
63
Enumeration of all possible API error codes returned by the Notion API.
64
65
```python { .api }
66
class APIErrorCode(str, Enum):
67
"""API error code enumeration."""
68
69
Unauthorized = "unauthorized"
70
"""The bearer token is not valid."""
71
72
RestrictedResource = "restricted_resource"
73
"""Given the bearer token used, the client doesn't have permission to perform this operation."""
74
75
ObjectNotFound = "object_not_found"
76
"""Given the bearer token used, the resource does not exist. This error can also indicate that the resource has not been shared with owner of the bearer token."""
77
78
RateLimited = "rate_limited"
79
"""This request exceeds the number of requests allowed. Slow down and try again."""
80
81
InvalidJSON = "invalid_json"
82
"""The request body could not be decoded as JSON."""
83
84
InvalidRequestURL = "invalid_request_url"
85
"""The request URL is not valid."""
86
87
InvalidRequest = "invalid_request"
88
"""This request is not supported."""
89
90
ValidationError = "validation_error"
91
"""The request body does not match the schema for the expected parameters."""
92
93
ConflictError = "conflict_error"
94
"""The transaction could not be completed, potentially due to a data collision. Make sure the parameters are up to date and try again."""
95
96
InternalServerError = "internal_server_error"
97
"""An unexpected error occurred. Reach out to Notion support."""
98
99
ServiceUnavailable = "service_unavailable"
100
"""Notion is unavailable. Try again later. This can occur when the time to respond to a request takes longer than 60 seconds, the maximum request timeout."""
101
```
102
103
### Error Utility Functions
104
105
```python { .api }
106
def is_api_error_code(code):
107
"""
108
Check if given code belongs to the list of valid API error codes.
109
110
Parameters:
111
- code: str, error code to check
112
113
Returns:
114
bool, True if code is a valid API error code
115
"""
116
```
117
118
## Usage Examples
119
120
### Basic Error Handling
121
122
```python
123
from notion_client import Client, APIResponseError, APIErrorCode
124
125
notion = Client(auth="your_token")
126
127
try:
128
page = notion.pages.retrieve(page_id="invalid_page_id")
129
except APIResponseError as error:
130
print(f"API Error: {error}")
131
print(f"Error Code: {error.code}")
132
print(f"HTTP Status: {error.status}")
133
print(f"Response Body: {error.body}")
134
```
135
136
### Specific Error Code Handling
137
138
```python
139
from notion_client import Client, APIResponseError, APIErrorCode
140
import logging
141
142
try:
143
response = notion.databases.query(
144
database_id="database_id_here",
145
filter={"property": "Status", "select": {"equals": "Active"}}
146
)
147
except APIResponseError as error:
148
if error.code == APIErrorCode.ObjectNotFound:
149
print("Database not found or not accessible")
150
elif error.code == APIErrorCode.Unauthorized:
151
print("Invalid or expired authentication token")
152
elif error.code == APIErrorCode.RestrictedResource:
153
print("Insufficient permissions to access this resource")
154
elif error.code == APIErrorCode.RateLimited:
155
print("Rate limit exceeded, please slow down requests")
156
elif error.code == APIErrorCode.ValidationError:
157
print("Invalid request parameters")
158
print(f"Details: {error.body}")
159
else:
160
logging.error(f"Unexpected API error: {error.code} - {error}")
161
```
162
163
### Comprehensive Error Handling
164
165
```python
166
from notion_client import (
167
Client,
168
APIResponseError,
169
HTTPResponseError,
170
RequestTimeoutError,
171
APIErrorCode
172
)
173
import time
174
import logging
175
176
def robust_api_call(notion, operation, max_retries=3):
177
"""
178
Make an API call with comprehensive error handling and retries.
179
180
Parameters:
181
- notion: Client instance
182
- operation: callable that performs the API operation
183
- max_retries: int, maximum number of retry attempts
184
185
Returns:
186
API response or None if all retries failed
187
"""
188
for attempt in range(max_retries + 1):
189
try:
190
return operation()
191
192
except APIResponseError as error:
193
if error.code == APIErrorCode.RateLimited:
194
if attempt < max_retries:
195
wait_time = 2 ** attempt # Exponential backoff
196
print(f"Rate limited. Waiting {wait_time} seconds before retry...")
197
time.sleep(wait_time)
198
continue
199
else:
200
print("Max retries reached for rate limiting")
201
raise
202
203
elif error.code == APIErrorCode.InternalServerError:
204
if attempt < max_retries:
205
wait_time = 2 ** attempt
206
print(f"Server error. Retrying in {wait_time} seconds...")
207
time.sleep(wait_time)
208
continue
209
else:
210
print("Max retries reached for server errors")
211
raise
212
213
elif error.code == APIErrorCode.ServiceUnavailable:
214
if attempt < max_retries:
215
wait_time = 5 * (attempt + 1) # Linear backoff for service issues
216
print(f"Service unavailable. Retrying in {wait_time} seconds...")
217
time.sleep(wait_time)
218
continue
219
else:
220
print("Max retries reached for service unavailability")
221
raise
222
223
elif error.code in [
224
APIErrorCode.Unauthorized,
225
APIErrorCode.ObjectNotFound,
226
APIErrorCode.RestrictedResource,
227
APIErrorCode.ValidationError
228
]:
229
# These errors won't be resolved by retrying
230
print(f"Non-retryable error: {error.code}")
231
raise
232
233
else:
234
print(f"Unknown API error: {error.code}")
235
raise
236
237
except RequestTimeoutError:
238
if attempt < max_retries:
239
wait_time = 2 ** attempt
240
print(f"Request timeout. Retrying in {wait_time} seconds...")
241
time.sleep(wait_time)
242
continue
243
else:
244
print("Max retries reached for timeouts")
245
raise
246
247
except HTTPResponseError as error:
248
print(f"HTTP error {error.status}: {error}")
249
if error.status >= 500 and attempt < max_retries:
250
# Retry server errors
251
wait_time = 2 ** attempt
252
print(f"Server error. Retrying in {wait_time} seconds...")
253
time.sleep(wait_time)
254
continue
255
else:
256
raise
257
258
except Exception as error:
259
print(f"Unexpected error: {error}")
260
raise
261
262
return None
263
264
# Usage example
265
notion = Client(auth="your_token")
266
267
def get_database():
268
return notion.databases.retrieve(database_id="database_id_here")
269
270
database = robust_api_call(notion, get_database)
271
if database:
272
print("Successfully retrieved database")
273
else:
274
print("Failed to retrieve database after all retries")
275
```
276
277
### Error Logging and Monitoring
278
279
```python
280
import logging
281
from notion_client import Client, APIResponseError, HTTPResponseError, RequestTimeoutError
282
283
# Set up logging
284
logging.basicConfig(
285
level=logging.INFO,
286
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
287
)
288
logger = logging.getLogger(__name__)
289
290
notion = Client(
291
auth="your_token",
292
log_level=logging.DEBUG # Enable debug logging for API requests
293
)
294
295
def log_api_error(error, operation_name):
296
"""Log API errors with relevant details."""
297
if isinstance(error, APIResponseError):
298
logger.error(
299
f"API Error in {operation_name}: "
300
f"Code={error.code}, Status={error.status}, "
301
f"Message={str(error)}"
302
)
303
if error.code == APIErrorCode.ValidationError:
304
logger.error(f"Validation details: {error.body}")
305
elif isinstance(error, RequestTimeoutError):
306
logger.error(f"Timeout in {operation_name}: {str(error)}")
307
elif isinstance(error, HTTPResponseError):
308
logger.error(
309
f"HTTP Error in {operation_name}: "
310
f"Status={error.status}, Message={str(error)}"
311
)
312
else:
313
logger.error(f"Unexpected error in {operation_name}: {str(error)}")
314
315
try:
316
users = notion.users.list()
317
logger.info(f"Successfully retrieved {len(users['results'])} users")
318
except (APIResponseError, HTTPResponseError, RequestTimeoutError) as error:
319
log_api_error(error, "users.list")
320
```
321
322
### Error Context and Recovery
323
324
```python
325
from notion_client import Client, APIResponseError, APIErrorCode
326
327
class NotionClientWrapper:
328
"""Wrapper class with enhanced error handling and context."""
329
330
def __init__(self, auth_token):
331
self.notion = Client(auth=auth_token)
332
self.last_error = None
333
334
def safe_database_query(self, database_id, **kwargs):
335
"""Query database with error context and fallback strategies."""
336
try:
337
return self.notion.databases.query(database_id=database_id, **kwargs)
338
339
except APIResponseError as error:
340
self.last_error = error
341
342
if error.code == APIErrorCode.ObjectNotFound:
343
print(f"Database {database_id} not found. Checking if it exists...")
344
return self._handle_not_found_database(database_id)
345
346
elif error.code == APIErrorCode.ValidationError:
347
print("Query validation failed. Trying with simplified query...")
348
return self._retry_with_simple_query(database_id)
349
350
elif error.code == APIErrorCode.RestrictedResource:
351
print("Access restricted. Trying to get basic database info...")
352
return self._get_basic_database_info(database_id)
353
354
else:
355
raise # Re-raise unhandled errors
356
357
def _handle_not_found_database(self, database_id):
358
"""Handle database not found scenario."""
359
try:
360
# Try to retrieve the database directly
361
db_info = self.notion.databases.retrieve(database_id=database_id)
362
print("Database exists but may be empty")
363
return {"results": [], "database_info": db_info}
364
except APIResponseError:
365
print("Database truly does not exist or is not accessible")
366
return None
367
368
def _retry_with_simple_query(self, database_id):
369
"""Retry with a simpler query."""
370
try:
371
return self.notion.databases.query(
372
database_id=database_id,
373
page_size=10 # Reduce page size and remove filters
374
)
375
except APIResponseError:
376
print("Even simple query failed")
377
return None
378
379
def _get_basic_database_info(self, database_id):
380
"""Get basic database information when full access is restricted."""
381
try:
382
db_info = self.notion.databases.retrieve(database_id=database_id)
383
return {"results": [], "database_info": db_info, "access_limited": True}
384
except APIResponseError:
385
return None
386
387
# Usage
388
wrapper = NotionClientWrapper("your_token")
389
result = wrapper.safe_database_query("database_id_here")
390
391
if result is None:
392
print("Could not access database")
393
elif "access_limited" in result:
394
print("Limited access - only database metadata available")
395
else:
396
print(f"Retrieved {len(result['results'])} pages")
397
```
398
399
## Error Prevention Best Practices
400
401
### Validation Before API Calls
402
403
```python
404
import re
405
from notion_client import Client
406
407
def is_valid_notion_id(notion_id):
408
"""Check if a string is a valid Notion ID format."""
409
# Notion IDs are UUIDs with or without hyphens
410
uuid_pattern = r'^[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}$'
411
return re.match(uuid_pattern, notion_id.lower()) is not None
412
413
def safe_page_retrieve(notion, page_id):
414
"""Safely retrieve a page with pre-validation."""
415
if not is_valid_notion_id(page_id):
416
raise ValueError(f"Invalid page ID format: {page_id}")
417
418
try:
419
return notion.pages.retrieve(page_id=page_id)
420
except APIResponseError as error:
421
if error.code == APIErrorCode.ObjectNotFound:
422
print(f"Page {page_id} does not exist or is not accessible")
423
raise
424
```
425
426
### Rate Limiting Prevention
427
428
```python
429
import time
430
from collections import deque
431
from notion_client import Client
432
433
class RateLimitedClient:
434
"""Client wrapper with built-in rate limiting."""
435
436
def __init__(self, auth_token, requests_per_second=3):
437
self.notion = Client(auth=auth_token)
438
self.requests_per_second = requests_per_second
439
self.request_times = deque()
440
441
def _enforce_rate_limit(self):
442
"""Enforce rate limiting before making requests."""
443
now = time.time()
444
445
# Remove requests older than 1 second
446
while self.request_times and now - self.request_times[0] > 1.0:
447
self.request_times.popleft()
448
449
# If we've made too many requests, wait
450
if len(self.request_times) >= self.requests_per_second:
451
sleep_time = 1.0 - (now - self.request_times[0])
452
if sleep_time > 0:
453
time.sleep(sleep_time)
454
455
self.request_times.append(now)
456
457
def query_database(self, database_id, **kwargs):
458
"""Query database with rate limiting."""
459
self._enforce_rate_limit()
460
return self.notion.databases.query(database_id=database_id, **kwargs)
461
462
# Usage
463
rate_limited_notion = RateLimitedClient("your_token", requests_per_second=2)
464
results = rate_limited_notion.query_database("database_id_here")
465
```