Error Handling
Error Handling
Handle API errors from the TypeScript SDKs using ApiError (admin SDK) and ModulaError (read-only SDK), plus standard TypeError for network failures.
ApiError (Admin SDK)
The admin SDK throws ApiError for non-2xx HTTP responses:
type ApiError = {
readonly _tag: 'ApiError'
status: number
message: string
body?: unknown
}
| Field | Description |
|---|---|
_tag |
Discriminant tag. Always 'ApiError'. |
status |
HTTP status code (e.g. 404, 403, 500). |
message |
HTTP status text (e.g. 'Not Found'). |
body |
Parsed JSON response body, if the server returned application/json. |
ApiError is a plain object, not an Error subclass. Use isApiError to narrow caught values:
import { isApiError } from '@modulacms/types'
try {
const user = await client.users.get(userId)
} catch (err) {
if (isApiError(err)) {
console.error(`API error ${err.status}: ${err.message}`)
if (err.body) {
console.error('Response body:', err.body)
}
}
}
isApiError
function isApiError(err: unknown): err is ApiError
Returns true if err is an object with _tag === 'ApiError'. Returns false for network errors, timeouts, and non-SDK exceptions.
ModulaError (Read-Only SDK)
The read-only SDK throws ModulaError, which extends the standard Error class:
class ModulaError extends Error {
status: number
body: unknown
get errorMessage(): string | undefined
}
| Field | Description |
|---|---|
status |
HTTP status code. |
body |
Parsed JSON response body, if available. |
message |
Inherited from Error. Contains a description of the failure. |
errorMessage |
Getter that extracts a string message from body, if present. |
Use instanceof to catch:
import { ModulaError } from '@modulacms/sdk'
try {
const page = await client.getPage('about')
} catch (err) {
if (err instanceof ModulaError) {
console.error(`Status ${err.status}: ${err.errorMessage ?? err.message}`)
}
}
Error Patterns
Checking Status Codes
try {
const item = await client.contentData.get(contentId)
} catch (err) {
if (isApiError(err)) {
switch (err.status) {
case 404:
// Resource not found
break
case 403:
// Insufficient permissions
break
case 409:
// Conflict (e.g. duplicate)
break
case 422:
// Validation error -- check err.body for details
break
default:
// Server error
break
}
}
}
Not Found Pattern
async function findContent(id: ContentID): Promise<ContentData | null> {
try {
return await client.contentData.get(id)
} catch (err) {
if (isApiError(err) && err.status === 404) {
return null
}
throw err
}
}
Network Errors
Network failures (DNS resolution, connection refused, TLS errors) surface as standard TypeError from the fetch API:
try {
const data = await client.users.list()
} catch (err) {
if (isApiError(err)) {
// Server returned an error response
} else if (err instanceof TypeError) {
// Network failure -- server unreachable
} else if (err instanceof DOMException && err.name === 'AbortError') {
// Request was cancelled (timeout or manual abort)
}
}
Timeout Handling
The admin SDK attaches AbortSignal.timeout(defaultTimeout) to every request (default 30 seconds). You can also pass a custom signal per request:
const controller = new AbortController()
setTimeout(() => controller.abort(), 5000) // 5 second timeout
try {
const data = await client.users.list({ signal: controller.signal })
} catch (err) {
if (err instanceof DOMException && err.name === 'AbortError') {
console.error('Request timed out')
}
}
When you provide both the default timeout signal and a custom signal, either one aborting cancels the request.
Media Upload Errors
The admin SDK provides helper functions for common media upload errors:
import {
isDuplicateMedia,
isFileTooLarge,
isInvalidMediaPath,
} from '@modulacms/admin-sdk'
try {
await client.mediaUpload.upload(file, { path: 'uploads' })
} catch (err) {
if (isDuplicateMedia(err)) {
// HTTP 409 -- file already exists
} else if (isFileTooLarge(err)) {
// HTTP 400 -- file exceeds size limit
} else if (isInvalidMediaPath(err)) {
// HTTP 400 -- path contains traversal or invalid characters
}
}
Void Responses
DELETE and some POST operations return void on success but throw ApiError on failure:
try {
await client.users.remove(userId)
// Success -- no return value
} catch (err) {
if (isApiError(err) && err.status === 403) {
console.error('Cannot delete system-protected user')
}
}