@anthropic-ai/sdk
semver
>=0.18.0 <1.0.0postconditions24functions12last verified2026-06-24coverage score67%Postconditions: what we check
- create · messages-create-no-try-catcherrorWhenmessages.create() called without try-catch or .catch() handlerThrows
APIError with error.status and error.messageRequired handlingMUST wrap await client.messages.create() in try-catch block. Catch block should check error instanceof Anthropic.APIError and handle specific error types (RateLimitError, AuthenticationError, etc.) appropriately.costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisibleSources[1] - create · messages-create-generic-catchwarningWhenmessages.create() in try-catch but doesn't check error typesRequired handlingSHOULD check error type using instanceof or error.status code. Handle RateLimitError with retry logic (using retry-after header), AuthenticationError by validating API key, and server errors (500/529) with appropriate backoff.costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisibleSources[2]
- stream · messages-stream-no-try-catcherrorWhenmessages.stream() called without try-catch or .catch() handlerThrows
APIError, can occur mid-stream after initial 200 responseRequired handlingMUST wrap await client.messages.stream() in try-catch block. Should also wrap stream iteration in try-catch to handle mid-stream errors. Check error types for proper recovery.costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisibleSources[1] - stream · stream-abort-not-handledinfoWhenStream created but no abort/cleanup handlingRequired handlingSHOULD implement cancellation handling with stream.controller.abort() or break from stream iteration. Use finally blocks for cleanup.costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisibleSources[2]
- Anthropic · api-key-not-validatedwarningWhenAnthropic client created without validating API key existsRequired handlingSHOULD validate API key exists before client creation: if (!process.env.ANTHROPIC_API_KEY) throw new Error('Missing ANTHROPIC_API_KEY'). Consider using default apiKey validation from SDK.costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisibleSources[1]
- Anthropic · rate-limit-no-retry-logicinfoWhenAPI calls made without retry logic for 429 errorsRequired handlingSHOULD implement retry logic for 429 errors. Check retry-after header value and implement exponential backoff. Example: if (error instanceof RateLimitError) wait for error.headers['retry-after'] seconds before retry.costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisibleSources[1]
- countTokens · count-tokens-no-try-catcherrorWhenmessages.countTokens() called without try-catch or .catch() handlerThrows
APIError (BadRequestError 400, AuthenticationError 401, RateLimitError 429, InternalServerError 500)Required handlingMUST wrap await client.messages.countTokens() in try-catch. Despite being a read-only operation, it can fail with auth errors (bad API key) or rate limit errors when count token RPM quota is exceeded. Token counting has its own independent rate limit separate from message creation.costlowin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - countTokens · count-tokens-estimate-used-as-exactwarningWhenToken count result used as exact limit without accounting for estimate varianceRequired handlingSHOULD add a safety margin (e.g., 5-10%) when using token counts as gates. Example: if (tokenCount.input_tokens * 1.05 > maxContextTokens) { ... } Do not use strict equality to gate on token counts.costlowin prodsilent failureusers seedegraded performancevisibilitysilentSources[3]
- batches.create · batches-create-no-try-catcherrorWhenmessages.batches.create() called without try-catch or .catch() handlerThrows
APIError (BadRequestError 400, AuthenticationError 401, RateLimitError 429, InternalServerError 500)Required handlingMUST wrap await client.messages.batches.create() in try-catch. Batch creation can fail with BadRequestError (400) if any individual request params are invalid, AuthenticationError (401) if API key invalid, or RateLimitError (429) for quota exceeded. Check error.status and implement retry with exponential backoff for 429/500/529 errors.costhighin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - batches.create · batches-result-not-polledwarningWhenBatch created but results never retrieved or polling logic absentRequired handlingMUST implement result retrieval after batch creation. Either poll messages.batches.retrieve() until processing_status === 'ended', then stream results with messages.batches.results(); or use a background job (e.g. Inngest, BullMQ) triggered by webhook or periodic cron to poll and process results. Never create a batch without a retrieval plan.costhighin prodsilent failureusers seelost datavisibilitysilentSources[4]
- batches.create · batches-individual-errors-not-handledwarningWhenBatch results retrieved but individual request error statuses not checkedRequired handlingMUST check result.type before accessing result.message: if (result.type === 'succeeded') { process(result.message) } else if (result.type === 'errored') { handleError(result.error) } else if (result.type === 'expired') { requeue(result.custom_id) } Track errored count and alert when error rate exceeds threshold.costmediumin prodsilent failureusers seelost datavisibilitysilentSources[4]
- beta.files.upload · files-upload-no-try-catcherrorWhenbeta.files.upload() called without try-catch or .catch() handlerThrows
APIError (BadRequestError 400, AuthenticationError 401, RateLimitError 429, InternalServerError 500, 413 file too large, 403 storage limit exceeded)Required handlingMUST wrap await client.beta.files.upload() in try-catch. Check error.status: 413 = file too large (enforce 500 MB limit before upload); 403 = organization storage limit reached (implement storage quota checks); 400 = invalid filename or file type (validate before upload); 429 = rate limit (implement backoff, beta limit is ~100 req/min).costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - beta.files.upload · files-upload-id-not-validatedwarningWhenFile ID from upload used in messages without checking upload succeededRequired handlingSHOULD verify upload result contains a valid id before using in messages: const file = await client.beta.files.upload({ file: ... }); if (!file.id) throw new Error('File upload returned no ID'); Then use file.id in the content block.costlowin prodimmediate exceptionusers seeservice unavailablevisibilityvisibleSources[5]
- messages.parse · messages-parse-no-try-catcherrorWhenmessages.parse() called without try-catch or .catch() handlerThrows
APIError (identical error set to messages.create()), plus ZodError if schema parse failsRequired handlingMUST wrap await client.messages.parse() in try-catch. Catch block should handle both APIError subclasses (instanceof Anthropic.APIError) and ZodError from schema validation failures. Note: a successful API call that produces output not matching the schema sets parsed_output to null rather than throwing — always check parsed_output !== null before using it.costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - messages.parse · messages-parse-null-output-uncheckedwarningWhenparsed_output used without null check after messages.parse()Required handlingMUST check parsed_output before using: const result = await client.messages.parse({ ... }); if (result.parsed_output === null) { // Handle refusal or incomplete output // Check result.stop_reason for diagnosis } else { use(result.parsed_output); }costlowin prodimmediate exceptionusers seelost datavisibilityvisibleSources[2]
- batches.cancel · batches-cancel-no-try-catcherrorWhenclient.messages.batches.cancel() called without try-catch or .catch() handlerThrows
APIError hierarchy: NotFoundError (404) if message_batch_id does not exist or belongs to a different organization; AuthenticationError (401) on bad API key; RateLimitError (429) on quota; InternalServerError (500/529); APIConnectionError on network failure.Required handlingMUST wrap await client.messages.batches.cancel(id) in try-catch. Treat NotFoundError as non-fatal in bulk-cancel loops (the batch was already gone — that's the desired end state). Propagate other errors: try { await client.messages.batches.cancel(batchId); } catch (err) { if (err instanceof Anthropic.NotFoundError) { // Already gone — log and continue } else if (err instanceof Anthropic.APIError) { throw new Error(`Batch cancel failed: ${err.status} ${err.message}`); } else { throw err; } }costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - batches.cancel · batches-cancel-state-not-recheckedwarningWhenbatches.cancel() resolves successfully but caller does not poll the returned MessageBatch.processing_status to confirm the batch actually finished (vs sitting in "canceling" state with non-interruptible requests still completing).Throws
No exception. The returned MessageBatch.processing_status will be "canceling" (in transit) or "ended" (fully done). Caller that immediately calls batches.delete() will get a 4xx for "cannot delete in-progress batch" if processing_status is not "ended".Required handlingAfter cancel, poll batches.retrieve(id) until processing_status === 'ended' before deleting or treating the batch as terminal: await client.messages.batches.cancel(batchId); let batch; do { await new Promise(r => setTimeout(r, 2000)); batch = await client.messages.batches.retrieve(batchId); } while (batch.processing_status !== 'ended'); // Now safe to delete or treat as finalcostlowin prodsilent failureusers seedegraded performancevisibilitysilentSources[6] - batches.delete · batches-delete-no-try-catcherrorWhenclient.messages.batches.delete() called without try-catch or .catch() handlerThrows
APIError hierarchy: BadRequestError (400 invalid_request_error) if the batch is still processing — Anthropic explicitly requires that the batch be in "ended" state before delete will succeed; NotFoundError (404) if the batch_id is invalid; AuthenticationError (401); RateLimitError (429); InternalServerError (500/529); APIConnectionError on network failure.Required handlingMUST wrap await client.messages.batches.delete(id) in try-catch. In batch deletion loops, log-and-continue on per-batch failure to ensure the worker completes: for (const id of batchIds) { try { await client.messages.batches.delete(id); } catch (err) { if (err instanceof Anthropic.BadRequestError) { // Likely still processing — cancel first, then retry later logger.warn('Skipping delete of in-progress batch', { id }); continue; } if (err instanceof Anthropic.NotFoundError) { continue; // already gone } logger.error('Batch delete failed', { id, err }); } }costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - batches.delete · batches-delete-without-prior-cancelwarningWhenbatches.delete() is called on a batch whose processing_status has not been verified to be "ended". The caller assumed the batch was complete but it was actually still in_progress or canceling.Throws
BadRequestError (400 invalid_request_error). The error message will indicate the batch is not in a deletable state.Required handlingMUST verify the batch is in "ended" state before deleting: const batch = await client.messages.batches.retrieve(id); if (batch.processing_status !== 'ended') { await client.messages.batches.cancel(id); // Poll until ended (see batches.cancel post-condition) } await client.messages.batches.delete(id);costlowin prodimmediate exceptionusers seedegraded performancevisibilityvisibleSources[7] - batches.results · batches-results-no-try-catcherrorWhenclient.messages.batches.results() awaited without try-catch on the await OR on the for-await iterationThrows
APIError on the initial fetch (404 NotFoundError if batch_id invalid, 400 BadRequestError if processing_status !== 'ended', 401/429/500). APIConnectionError or generic Error during streaming iteration if the JSONL connection drops mid-stream.Required handlingMUST wrap BOTH the await and the iteration: try { const decoder = await client.messages.batches.results(batchId); try { for await (const item of decoder) { // Each item also needs per-result handling: if (item.result.type === 'errored') { handleErrored(item.custom_id, item.result.error); } else if (item.result.type === 'expired' || item.result.type === 'canceled') { handleNonSuccess(item.custom_id, item.result.type); } else { handleSucceeded(item.custom_id, item.result.message); } } } catch (streamErr) { // Connection drop mid-stream — note how many results were // processed and retry from that custom_id logger.error('Batch results stream interrupted', { streamErr }); } } catch (fetchErr) { if (fetchErr instanceof Anthropic.BadRequestError) { // Batch not in "ended" state yet } throw fetchErr; }costhighin prodimmediate exceptionusers seelost datavisibilityvisible - batches.results · batches-results-individual-not-checkederrorWhenfor-await over batches.results() iterates items without checking item.result.type. Code treats every iterated item as a success and accesses item.result.message, which is undefined for type === 'errored' | 'canceled' | 'expired'.Throws
TypeError at runtime: "Cannot read properties of undefined (reading 'content')" when accessing item.result.message.content on a non-succeeded result. The error type union for MessageBatchIndividualResponse is discriminated by result.type — accessing .message on the wrong branch is a TypeScript-level error that the runtime cannot distinguish without a narrowing check.Required handlingMUST narrow on item.result.type before accessing fields: for await (const item of decoder) { switch (item.result.type) { case 'succeeded': use(item.result.message); break; case 'errored': handleError(item.custom_id, item.result.error); break; case 'canceled': case 'expired': scheduleRetry(item.custom_id); break; } }costhighin prodsilent failureusers seelost datavisibilitysilentSources[6] - beta.webhooks.unwrap · webhooks-unwrap-no-try-catcherrorWhenclient.beta.webhooks.unwrap(body, { headers }) called without try-catch. unwrap is synchronous but can throw THREE distinct error types: plain Error (missing key), WebhookVerificationError (bad signature), or SyntaxError (JSON.parse failure on malformed body).Throws
- Error('Webhook key must not be null in order to unwrap') if no webhookKey is configured on the client and no `key` option is passed. - WebhookVerificationError from standardwebhooks if the signature doesn't verify OR the webhook-timestamp header is outside the allowed clock-skew window. - SyntaxError from JSON.parse(body) if the body bytes aren't valid JSON.Required handlingMUST wrap in try-catch and distinguish the three failure modes: import { WebhookVerificationError } from 'standardwebhooks'; try { const event = client.beta.webhooks.unwrap(rawBody, { headers: req.headers, // key: process.env.ANTHROPIC_WEBHOOK_SIGNING_KEY }); await handleEvent(event); res.status(200).end(); } catch (err) { if (err instanceof WebhookVerificationError) { // Forged or expired — log + 401, do NOT 200 logger.warn('webhook verify failed', { err }); res.status(401).end(); return; } if (err instanceof SyntaxError) { // Malformed body — 400 res.status(400).end(); return; } // Missing key configuration — log loudly + 500 logger.error('webhook key not configured', { err }); res.status(500).end(); } NEVER return 200 from a verification-failed branch — Anthropic uses 200 as the "accepted" signal and stops retrying. A 4xx tells Anthropic the webhook was rejected and the next legitimate event is still delivered cleanly.costhighin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - beta.messages.toolRunner · tool-runner-no-try-catcherrorWhenclient.beta.messages.toolRunner() result is awaited or iterated without try-catch wrapping the await OR the for-await loop.Throws
APIError hierarchy on each underlying messages.create() call (BadRequestError, AuthenticationError, RateLimitError, InternalServerError, APIConnectionError); AnthropicError thrown by the runner itself ('ToolRunner concluded without a message from the server', 'Cannot iterate over a consumed stream'); whatever error the user's tool callback throws (re-thrown by the runner).Required handlingMUST wrap the await OR the for-await iteration in try-catch: try { const runner = client.beta.messages.toolRunner({ model: 'claude-opus-4-6', max_tokens: 1024, max_iterations: 10, tools: [...], messages: [...], }); for await (const msg of runner) { process(msg); } // OR: const final = await runner.runUntilDone(); } catch (err) { if (err instanceof Anthropic.APIError) { // Underlying messages.create() failed mid-loop } else if (err instanceof Anthropic.AnthropicError) { // Runner-level error (consumed stream, no message returned) } else { // User tool-callback threw — re-raised by runner } throw err; } Treat partial completion as lost work — the runner does not checkpoint, so a mid-loop failure requires the caller to re-seed the conversation history.costhighin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - beta.messages.toolRunner · tool-runner-max-iterations-not-setwarningWhenbeta.messages.toolRunner() is called without max_iterations set, relying on the SDK default or no termination guard. The model can stay in tool_use → tool_result → tool_use loops indefinitely if tool callbacks always succeed and the model never returns a text-only final message.Throws
No exception directly. The runner consumes tokens (and dollars) until the API itself throws (RateLimitError 429, or context window InternalServerError) or the surrounding worker times out. Silent runaway cost is the dominant failure mode.Required handlingSHOULD set max_iterations explicitly: const runner = client.beta.messages.toolRunner({ model: 'claude-opus-4-6', max_tokens: 1024, max_iterations: 10, // hard cap tools: [...], messages: [...], }); OR provide an AbortSignal with a timeout: const controller = new AbortController(); setTimeout(() => controller.abort(), 60_000); const runner = client.beta.messages.toolRunner( { ... }, { signal: controller.signal }, ); For untrusted-input agentic loops, both should be set.costhighin prodsilent failureusers seedegraded performancevisibilitysilent
Sources
Every postcondition cites at least one of these. Grouped by source type; numbered to match the footnotes above.
Official documentation
- [1]platform.claude.com/docs/en/apiErrors
- [3]platform.claude.com/docs/en/build-with-claudeToken Counting
- [4]platform.claude.com/docs/en/build-with-claudeBatch Processing
- [5]platform.claude.com/docs/en/build-with-claudeFiles
- [6]platform.claude.com/docs/en/apiCanceling Message Batches
- [7]platform.claude.com/docs/en/apiDeleting Message Batches
- [8]standardwebhooks.comstandardwebhooks.com
- [9]docs.claude.com/en/docs/agents-and-toolsOverview
Source code
- [2]github.com/anthropics/anthropic-sdk-typescriptanthropics/anthropic-sdk-typescript
Research notes
Curator notes from SOURCES.md captured when the profile was written so you can verify the reasoning, not just the rules.
Sources: @anthropic-ai/sdk Nark profile
Package: @anthropic-ai/sdk Version: >=0.18.0 <1.0.0 Research Date: 2026-02-24
Official Documentation
API Error Documentation
- URL: https://platform.claude.com/docs/en/api/errors
- Key Points:
- 8 HTTP error codes: 400, 401, 403, 404, 413, 429, 500, 529
- Error shapes include type, message, and request_id
- Rate limit errors include retry-after header
- Streaming can fail mid-response after 200 status
- Request size limits: 32MB (Messages), 256MB (Batch), 500MB (Files)
npm Package
- URL: https://www.npmjs.com/package/@anthropic-ai/sdk
- Key Points:
- TypeScript definitions included
- Comprehensive error hierarchy
- Support for streaming and batch processing
- MCP (Model Context Protocol) helpers
GitHub SDK Repository
- URL: https://github.com/anthropics/anthropic-sdk-typescript
- Key Points:
- Error classes: APIError, RateLimitError, AuthenticationError
- ToolError for structured tool error reporting
- UnsupportedMCPValueError for MCP helpers
- Streaming cancellation via stream.controller.abort()
HTTP Error Codes
400 - invalid_request_error
- Cause: Malformed request, missing required fields, invalid parameters
- Solution: Validate request structure before sending
- Example: Prefill not supported on Opus 4.6, invalid message format
401 - authentication_error
- Cause: Invalid or missing API key
- Solution: Validate ANTHROPIC_API_KEY environment variable
- Prevention: Check API key exists before client initialization
403 - permission_error
- Cause: API key lacks permission for requested resource
- Solution: Verify account permissions and model access
- Prevention: Use appropriate API key for resource
404 - not_found_error
- Cause: Requested resource doesn't exist
- Solution: Verify resource IDs and model names
- Example: Invalid model name, missing batch ID
413 - request_too_large
- Cause: Request exceeds maximum size (32MB for messages API)
- Solution: Split large requests or use Batch API (256MB limit)
- Prevention: Validate payload size before sending
429 - rate_limit_error
- Cause: Exceeded rate limits (RPM, ITPM, OTPM)
- Solution: Implement retry with retry-after header
- Prevention: Rate limit requests, implement queuing
- Tiers:
- Tier 1: 50 RPM (requires $5 credit)
- Tier 2: 60 RPM (requires $40)
- Tier 3: 300 RPM (requires $200)
- Tier 4: 4000 RPM (requires $400)
500 - api_error
- Cause: Unexpected internal error on Anthropic's servers
- Solution: Retry with exponential backoff
- Prevention: Implement robust error handling and retry logic
529 - overloaded_error
- Cause: API temporarily overloaded (high traffic)
- Solution: Implement exponential backoff and retry
- Prevention: Gradual traffic ramp-up, avoid sudden spikes
SDK Error Classes
APIError (Base Class)
if (error instanceof Anthropic.APIError) {
console.error('API Error:', error.message);
console.error('Status:', error.status);
console.error('Request ID:', error.headers['request-id']);
}
RateLimitError
if (error instanceof Anthropic.RateLimitError) {
const retryAfter = error.headers['retry-after'];
console.log(`Rate limited. Retry after ${retryAfter} seconds`);
// Implement exponential backoff
}
AuthenticationError
if (error instanceof Anthropic.AuthenticationError) {
throw new Error('Invalid ANTHROPIC_API_KEY. Check environment variable.');
}
ToolError
import { ToolError } from '@anthropic-ai/sdk/lib/tools/BetaRunnableTool';
// Report tool execution errors to model
throw new ToolError('Invalid input: URL is malformed');
// Include images in error reports
throw new ToolError([
{ type: 'text', text: 'Failed to load page' },
{ type: 'image', source: { type: 'base64', data: screenshot, media_type: 'image/png' } }
]);
UnsupportedMCPValueError
import { UnsupportedMCPValueError, mcpResourceToContent } from '@anthropic-ai/sdk/helpers/beta/mcp';
try {
const content = mcpResourceToContent(resource);
} catch (error) {
if (error instanceof UnsupportedMCPValueError) {
console.error('Unsupported MCP value:', error.message);
}
}
Recommended Error Handling Patterns
Pattern 1: Basic Try-Catch with Type Checking
import Anthropic from '@anthropic-ai/sdk';
const anthropic = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
});
try {
const message = await anthropic.messages.create({
model: 'claude-sonnet-4-5-20250929',
max_tokens: 1024,
messages: [{ role: 'user', content: 'Hello, Claude' }],
});
console.log(message.content);
} catch (error) {
if (error instanceof Anthropic.APIError) {
console.error('API Error:', error.status, error.message);
console.error('Request ID:', error.headers['request-id']);
} else {
console.error('Unexpected error:', error);
}
}
Pattern 2: Differentiated Error Handling
try {
const message = await anthropic.messages.create({
model: 'claude-opus-4-6',
max_tokens: 1024,
messages: [{ role: 'user', content: 'Hello' }],
});
} catch (error) {
if (error instanceof Anthropic.RateLimitError) {
// Rate limit - implement retry
const retryAfter = parseInt(error.headers['retry-after'] || '60');
console.log(`Rate limited. Retry in ${retryAfter}s`);
await delay(retryAfter * 1000);
// Retry request
} else if (error instanceof Anthropic.AuthenticationError) {
// Auth error - fix API key
throw new Error('Invalid API key. Check ANTHROPIC_API_KEY environment variable.');
} else if (error instanceof Anthropic.APIError) {
// Server error or other API issue
if (error.status === 500 || error.status === 529) {
console.log('Server error. Implementing backoff...');
// Exponential backoff
} else {
console.error('API Error:', error.status, error.message);
}
} else {
throw error;
}
}
Pattern 3: Streaming with Error Handling
try {
const stream = await anthropic.messages.stream({
model: 'claude-sonnet-4-5-20250929',
max_tokens: 1024,
messages: [{ role: 'user', content: 'Write a story' }],
});
try {
for await (const chunk of stream) {
if (chunk.type === 'content_block_delta') {
process.stdout.write(chunk.delta.text);
}
}
} catch (streamError) {
console.error('Stream error:', streamError);
stream.controller.abort();
} finally {
// Cleanup
}
} catch (error) {
if (error instanceof Anthropic.APIError) {
console.error('Failed to start stream:', error.status, error.message);
}
}
Pattern 4: API Key Validation
function createAnthropicClient() {
const apiKey = process.env.ANTHROPIC_API_KEY;
if (!apiKey || apiKey.trim() === '') {
throw new Error('ANTHROPIC_API_KEY environment variable is required');
}
return new Anthropic({ apiKey });
}
Pattern 5: Retry with Exponential Backoff
async function createMessageWithRetry(
anthropic: Anthropic,
params: any,
maxRetries = 3
) {
let retries = 0;
while (retries < maxRetries) {
try {
return await anthropic.messages.create(params);
} catch (error) {
if (error instanceof Anthropic.RateLimitError) {
const retryAfter = parseInt(error.headers['retry-after'] || '1');
const backoff = Math.min(retryAfter * 1000, Math.pow(2, retries) * 1000);
console.log(`Rate limited. Waiting ${backoff}ms before retry ${retries + 1}/${maxRetries}`);
await new Promise(resolve => setTimeout(resolve, backoff));
retries++;
} else if (error instanceof Anthropic.APIError && (error.status === 500 || error.status === 529)) {
// Server error - exponential backoff
const backoff = Math.pow(2, retries) * 1000;
console.log(`Server error. Waiting ${backoff}ms before retry ${retries + 1}/${maxRetries}`);
await new Promise(resolve => setTimeout(resolve, backoff));
retries++;
} else {
// Non-retryable error
throw error;
}
}
}
throw new Error(`Max retries (${maxRetries}) exceeded`);
}
CVE & Security Analysis
CVE-2025-49596 (MCP Inspector, NOT SDK)
- URL: https://www.oligo.security/blog/critical-rce-vulnerability-in-anthropic-mcp-inspector-cve-2025-49596
- CVSS: 9.4/10.0 (Critical)
- Affected: MCP Inspector versions <0.14.1
- NOT Affected: @anthropic-ai/sdk package itself
- Issue: Remote code execution via unauthenticated MCP proxy requests
- Fix: Upgrade MCP Inspector to 0.14.1+
- Mitigation: Session tokens and origin checks added
SQL Injection in SQLite MCP Server
- URL: Trend Micro research (unpatched)
- Affected: SQLite MCP server component
- NOT Affected: @anthropic-ai/sdk package itself
- Impact: Malicious prompt injection, data exfiltration
SDK Security Status
- @anthropic-ai/sdk: No direct CVEs reported
- Status: Actively maintained by Anthropic
- Recommendation: Keep SDK updated, monitor dependencies
Real-World Usage Analysis
chatbot-ui Repository
- File:
app/api/chat/anthropic/route.ts - Usage: Edge runtime, streaming responses
- Patterns Observed:
- ✅ Uses try-catch blocks
- ❌ Uses generic
error: anycatches - ❌ Doesn't check error types (APIError, RateLimitError)
- ❌ No retry logic for rate limits
- ❌ No specific error status checking
- ❌ Returns generic 500 for all errors
Violation Example:
// Current (bad) pattern
try {
const response = await anthropic.messages.create({...});
} catch (error: any) {
console.error("Error calling Anthropic API:", error);
return new NextResponse(
JSON.stringify({ message: "An error occurred" }),
{ status: 500 }
);
}
Should be:
try {
const response = await anthropic.messages.create({...});
} catch (error) {
if (error instanceof Anthropic.RateLimitError) {
return new NextResponse(
JSON.stringify({ message: "Rate limit exceeded", retryAfter: error.headers['retry-after'] }),
{ status: 429 }
);
} else if (error instanceof Anthropic.AuthenticationError) {
return new NextResponse(
JSON.stringify({ message: "Invalid API key" }),
{ status: 401 }
);
} else if (error instanceof Anthropic.APIError) {
return new NextResponse(
JSON.stringify({ message: error.message, requestId: error.headers['request-id'] }),
{ status: error.status || 500 }
);
}
throw error;
}
Testing Strategy
Test Scenarios
- Valid message creation (should succeed)
- Invalid API key (should throw AuthenticationError, 401)
- Rate limit exceeded (should throw RateLimitError, 429)
- Request too large (should throw APIError, 413)
- Server error (should throw APIError, 500)
- Overloaded API (should throw APIError, 529)
- Streaming mid-failure (should handle gracefully)
Fixture Coverage
- proper-error-handling.ts: Demonstrates correct try-catch with error type checking
- missing-error-handling.ts: No try-catch (should trigger violations)
- generic-catch.ts: Try-catch without error type checking (should warn)
References
Official Documentation:
SDK Repository:
Security:
Research Notes
Completed: 2026-02-24 Researcher: Claude Sonnet 4.5 Quality: High - Official documentation and real-world usage reviewed Coverage: Comprehensive - All major error scenarios identified
Need a different package?
Request a profile