Skip to content

Error Handling

The SDK has 4 error classes. Each class handles a different type of failure.


Each SDK error class extends the built-in Error class:

import {
NotionValidationError, // Client-side validation failures
NotionAPIError, // API responses with error status
NotionNetworkError, // Network or connection failures
NotionRequestTimeoutError, // Request timeout
} from '@visus-io/notion-sdk-ts';

The SDK throws NotionValidationError when client-side validation fails. This check happens before the SDK sends a request to the API.

  • Rich text content exceeds 2,000 characters.
  • A URL exceeds 2,000 characters.
  • An array exceeds 100 elements.
  • The input data is invalid.
error.message; // Description of validation failure
import { NotionValidationError, richText } from '@visus-io/notion-sdk-ts';
try {
// Text exceeds 2,000 character limit
const text = 'a'.repeat(3000);
richText(text).build();
} catch (error) {
if (error instanceof NotionValidationError) {
console.error('Validation error:', error.message);
// "Rich text content exceeds maximum length of 2000 characters"
}
}
try {
await notion.blocks.children.append('page-id', {
children: Array(150).fill(block.paragraph('Too many blocks')),
});
} catch (error) {
if (error instanceof NotionValidationError) {
console.error('Request validation failed:');
console.error(error.message);
// Split into smaller batches
}
}

See Request Size Limits for all enforced limits.


The SDK throws NotionAPIError when the Notion API returns an error response. The response has a 4xx or 5xx status code.

error.status; // HTTP status code (e.g., 404, 401, 429)
error.code; // Notion error code (e.g., 'object_not_found')
error.message; // Error message from API
error.retryAfterMs; // Wait time before retry, in milliseconds (rate-limited and overload responses)
error.rateLimitReason; // Why the API rate-limited the request (rate-limited responses only)
error.isNotFound(); // 404: resource not found
error.isUnauthorized(); // 401: invalid token or missing permissions
error.isValidationError(); // 400: invalid request data
error.isRateLimited(); // 429: rate limit exceeded
error.isServiceOverloaded(); // 529: Notion is overloaded
error.isServerError(); // 5xx: Notion server error, includes 529
error.isRetryable(); // rate limited or server error

Retry behavior: The SDK retries every error that isRetryable() reports. It retries rate-limited (429) responses only when retryOnRateLimit is enabled. It always retries service-overload (529) responses and transient server errors (500 to 599), no matter the value of retryOnRateLimit. See Configuration. The Notion API recommends this retry behavior.

Exception: notion.pages.updateMarkdown() does not retry a 504 gateway_timeout automatically, unlike other transient server errors. A 504 does not guarantee that the write failed, so check the page content before you retry.

Rate limit reason: A rate-limited response may add a rate_limit_reason value to additional_data. Read it from error.rateLimitReason. Notion documents these values:

  • public_api_request_rate_limit — a per-connection limit
  • public_api_space_request_rate_limit — a shared workspace limit
  • public_api_endpoint_rate_limit — a limit on one endpoint
  • mcp_tool_rate_limit
  • collection_router_upstream_429
  • public_api_request_blocked

Notion may add new values over time. Treat an unrecognized value the same as any other rate_limited response. error.rateLimitReason still holds the string. isRetryable() still returns its normal result.

A public_api_request_blocked request cannot succeed on retry. isRetryable() returns false for this reason, so the SDK does not retry it automatically.

import { NotionAPIError } from '@visus-io/notion-sdk-ts';
try {
await notion.pages.retrieve('invalid-page-id');
} catch (error) {
if (error instanceof NotionAPIError) {
console.error(`API Error ${error.status}:`, error.message);
console.error('Error code:', error.code);
if (error.isNotFound()) {
console.error('Page does not exist or bot lacks access');
} else if (error.isUnauthorized()) {
console.error('Invalid token or missing permissions');
} else if (error.isRateLimited()) {
console.error('Rate limited, retry after delay');
}
}
}
try {
await notion.databases.query('database-id');
} catch (error) {
if (error instanceof NotionAPIError) {
switch (error.code) {
case 'object_not_found':
console.error('Database not found');
break;
case 'unauthorized':
console.error('Token invalid or database not shared');
break;
case 'rate_limited':
console.error('Rate limited, wait before retrying');
break;
case 'validation_error':
console.error('Invalid filter or sort parameters');
break;
default:
console.error('API error:', error.message);
}
}
}

The SDK throws NotionNetworkError when a network-level error occurs. Examples: a DNS failure or a refused connection.

error.message; // Error description
error.cause; // Underlying error object
  • The DNS lookup fails.
  • The connection is refused.
  • The network request times out.
  • A TLS or SSL error occurs.
import { NotionNetworkError } from '@visus-io/notion-sdk-ts';
try {
await notion.pages.retrieve('page-id');
} catch (error) {
if (error instanceof NotionNetworkError) {
console.error('Network error:', error.message);
console.error('Cause:', error.cause);
// Retry with exponential backoff or alert monitoring
}
}

The SDK throws NotionRequestTimeoutError when a request exceeds the configured timeout.

error.message; // Timeout description
import { NotionRequestTimeoutError } from '@visus-io/notion-sdk-ts';
const notion = new Notion({
auth: process.env.NOTION_TOKEN,
timeoutMs: 5_000, // 5 second timeout
});
try {
await notion.databases.query('large-database-id');
} catch (error) {
if (error instanceof NotionRequestTimeoutError) {
console.error('Request timed out:', error.message);
// Increase timeout or optimize query
}
}

import {
NotionAPIError,
NotionNetworkError,
NotionRequestTimeoutError,
NotionValidationError,
} from '@visus-io/notion-sdk-ts';
async function safeRetrievePage(pageId: string) {
try {
return await notion.pages.retrieve(pageId);
} catch (error) {
if (error instanceof NotionValidationError) {
console.error('Validation failed:', error.message);
throw new Error('Invalid input data');
} else if (error instanceof NotionAPIError) {
console.error(`API Error ${error.status}:`, error.message);
if (error.isNotFound()) {
return null; // Page does not exist
} else if (error.isUnauthorized()) {
throw new Error('Authentication failed');
} else if (error.isRateLimited()) {
// Wait and retry
await new Promise((resolve) => setTimeout(resolve, 5000));
return safeRetrievePage(pageId); // Retry once
} else if (error.isServerError()) {
throw new Error('Notion server error, try again later');
}
throw error;
} else if (error instanceof NotionNetworkError) {
console.error('Network error:', error.message);
throw new Error('Network connection failed');
} else if (error instanceof NotionRequestTimeoutError) {
console.error('Request timed out:', error.message);
throw new Error('Request took too long');
}
throw error; // Unknown error
}
}
async function retryWithBackoff<T>(
fn: () => Promise<T>,
maxRetries = 3,
baseDelay = 1000,
): Promise<T> {
let lastError: Error | undefined;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error as Error;
// Only retry on retryable errors
if (error instanceof NotionAPIError && (error.isRateLimited() || error.isServerError())) {
if (attempt < maxRetries) {
const delay = baseDelay * Math.pow(2, attempt);
console.log(`Retry attempt ${attempt + 1} after ${delay}ms`);
await new Promise((resolve) => setTimeout(resolve, delay));
continue;
}
}
throw error; // Do not retry other errors
}
}
throw lastError;
}
// Usage
const page = await retryWithBackoff(() => notion.pages.retrieve('page-id'));
async function getPageTitle(pageId: string): Promise<string> {
try {
const page = await notion.pages.retrieve(pageId);
return page.getTitle();
} catch (error) {
if (error instanceof NotionAPIError && error.isNotFound()) {
return 'Untitled'; // Default value
}
throw error; // Re-throw other errors
}
}
async function bulkUpdatePages(pageIds: string[]) {
const results = {
success: [] as string[],
failed: [] as { id: string; error: string }[],
};
for (const pageId of pageIds) {
try {
await notion.pages.update(pageId, {
properties: { Status: { status: { name: 'Done' } } },
});
results.success.push(pageId);
} catch (error) {
let errorMessage = 'Unknown error';
if (error instanceof NotionAPIError) {
errorMessage = `${error.code}: ${error.message}`;
} else if (error instanceof NotionNetworkError) {
errorMessage = 'Network error';
}
results.failed.push({ id: pageId, error: errorMessage });
}
}
console.log(`Success: ${results.success.length}, Failed: ${results.failed.length}`);
return results;
}

This table lists the Notion API error codes:

Code Status Description
invalid_json 400 Request body is not valid JSON
invalid_request_url 400 Invalid request URL
invalid_request 400 Request is invalid
validation_error 400 Validation failed on request parameters
missing_version 400 Notion-Version header missing
unauthorized 401 Invalid token or insufficient permissions
restricted_resource 403 Forbidden access to resource
object_not_found 404 Resource not found
conflict_error 409 Conflict with existing resource state
rate_limited 429 Too many requests (retried if retryOnRateLimit is enabled)
internal_server_error 500 Notion internal error (always auto-retried)
service_unavailable 503 Service temporarily unavailable (always auto-retried)
database_connection_unavailable 503 Database connection error (always auto-retried)
gateway_timeout 504 Gateway timeout (always auto-retried, except updateMarkdown())
service_overload 529 Notion is overloaded (always auto-retried)

Notion may add new codes over time. Treat an unrecognized code the same as any other error response. error.code still holds the string. Read error.status to classify the error.

404 Not Found (object_not_found):

  • The page or database does not exist.
  • The bot does not have access to the resource.
  • Someone deleted the resource.

401 Unauthorized (unauthorized):

  • The integration token is invalid.
  • The token expired.
  • The integration is not connected to the page or database.

400 Validation Error (validation_error):

  • The property type is invalid.
  • The filter syntax is invalid.
  • A required field is missing.

429 Rate Limited (rate_limited):

  • Too many requests occurred in a short time.
  • The SDK retries the request automatically. You can configure this behavior.

500 and 503 Server Errors:

  • A temporary Notion service issue occurred.
  • The SDK retries the request automatically for retryable operations.