The Anthropic Java SDK provides convenient access to the Anthropic REST API from applications written in Java
The Anthropic Java SDK provides a comprehensive exception hierarchy for handling different types of errors that may occur during API interactions. All exceptions are unchecked (extending RuntimeException) to provide flexibility in error handling without forcing verbose exception declarations.
All Anthropic SDK exceptions extend from the base class:
package com.anthropic.errors;
public class AnthropicException extends RuntimeException {
public AnthropicException(String message);
public AnthropicException(String message, Throwable cause);
}The exception hierarchy is organized into several categories:
AnthropicException (base)
├── AnthropicServiceException (HTTP errors)
│ ├── BadRequestException (400)
│ ├── UnauthorizedException (401)
│ ├── PermissionDeniedException (403)
│ ├── NotFoundException (404)
│ ├── UnprocessableEntityException (422)
│ ├── RateLimitException (429)
│ ├── InternalServerException (5xx)
│ ├── UnexpectedStatusCodeException (other status codes)
│ └── SseException (streaming errors)
├── AnthropicIoException (network/I/O errors)
├── AnthropicRetryableException (retryable failures)
└── AnthropicInvalidDataException (parsing/validation errors)Service exceptions represent HTTP errors returned by the Anthropic API. All service exceptions extend AnthropicServiceException:
package com.anthropic.errors;
import com.anthropic.core.http.Headers;
public class AnthropicServiceException extends AnthropicException {
private final int statusCode;
private final Headers headers;
public int statusCode();
public Headers headers();
}Indicates invalid request parameters or malformed request data:
package com.anthropic.errors;
public class BadRequestException extends AnthropicServiceException {
public BadRequestException(
int statusCode,
Headers headers,
String message,
Throwable cause
);
}Common causes:
Example:
import com.anthropic.errors.BadRequestException;
import com.anthropic.models.messages.MessageCreateParams;
import com.anthropic.models.messages.Model;
try {
MessageCreateParams params = MessageCreateParams.builder()
.maxTokens(0L) // Invalid: must be positive
.addUserMessage("Hello")
.model(Model.CLAUDE_SONNET_4_20250514)
.build();
client.messages().create(params);
} catch (BadRequestException e) {
System.err.println("Bad request: " + e.getMessage());
System.err.println("Status code: " + e.statusCode());
}Indicates authentication failure:
package com.anthropic.errors;
public class UnauthorizedException extends AnthropicServiceException {
public UnauthorizedException(
int statusCode,
Headers headers,
String message,
Throwable cause
);
}Common causes:
Example:
import com.anthropic.errors.UnauthorizedException;
try {
Message message = client.messages().create(params);
} catch (UnauthorizedException e) {
System.err.println("Authentication failed: " + e.getMessage());
// Log for debugging, check API key configuration
}Indicates insufficient permissions for the requested operation:
package com.anthropic.errors;
public class PermissionDeniedException extends AnthropicServiceException {
public PermissionDeniedException(
int statusCode,
Headers headers,
String message,
Throwable cause
);
}Common causes:
Example:
import com.anthropic.errors.PermissionDeniedException;
try {
Message message = client.messages().create(params);
} catch (PermissionDeniedException e) {
System.err.println("Permission denied: " + e.getMessage());
// Check account permissions and model access
}Indicates the requested resource does not exist:
package com.anthropic.errors;
public class NotFoundException extends AnthropicServiceException {
public NotFoundException(
int statusCode,
Headers headers,
String message,
Throwable cause
);
}Common causes:
Example:
import com.anthropic.errors.NotFoundException;
try {
MessageBatch batch = client.messages().batches().retrieve("invalid-id");
} catch (NotFoundException e) {
System.err.println("Resource not found: " + e.getMessage());
// Handle missing resource gracefully
}Indicates the request was well-formed but contains semantic errors:
package com.anthropic.errors;
public class UnprocessableEntityException extends AnthropicServiceException {
public UnprocessableEntityException(
int statusCode,
Headers headers,
String message,
Throwable cause
);
}Common causes:
Example:
import com.anthropic.errors.UnprocessableEntityException;
try {
Message message = client.messages().create(params);
} catch (UnprocessableEntityException e) {
System.err.println("Unprocessable entity: " + e.getMessage());
// Review request parameters and tool definitions
}Indicates rate limit has been exceeded:
package com.anthropic.errors;
public class RateLimitException extends AnthropicServiceException {
public RateLimitException(
int statusCode,
Headers headers,
String message,
Throwable cause
);
}Common causes:
Example:
import com.anthropic.errors.RateLimitException;
import java.util.Optional;
try {
Message message = client.messages().create(params);
} catch (RateLimitException e) {
System.err.println("Rate limit exceeded: " + e.getMessage());
// Check for retry-after header
Optional<String> retryAfter = e.headers().get("retry-after").stream().findFirst();
if (retryAfter.isPresent()) {
int seconds = Integer.parseInt(retryAfter.get());
System.out.println("Retry after " + seconds + " seconds");
}
}Indicates a server-side error:
package com.anthropic.errors;
public class InternalServerException extends AnthropicServiceException {
public InternalServerException(
int statusCode,
Headers headers,
String message,
Throwable cause
);
}Common causes:
Example:
import com.anthropic.errors.InternalServerException;
try {
Message message = client.messages().create(params);
} catch (InternalServerException e) {
System.err.println("Server error: " + e.getMessage());
System.err.println("Status code: " + e.statusCode());
// Implement retry logic with exponential backoff
}Handles HTTP status codes not covered by specific exception types:
package com.anthropic.errors;
public class UnexpectedStatusCodeException extends AnthropicServiceException {
public UnexpectedStatusCodeException(
int statusCode,
Headers headers,
String message,
Throwable cause
);
}Example:
import com.anthropic.errors.UnexpectedStatusCodeException;
try {
Message message = client.messages().create(params);
} catch (UnexpectedStatusCodeException e) {
System.err.println("Unexpected status code: " + e.statusCode());
System.err.println("Message: " + e.getMessage());
}Thrown for errors that occur during Server-Sent Events (SSE) streaming after a successful initial HTTP response:
package com.anthropic.errors;
public class SseException extends AnthropicServiceException {
public SseException(
int statusCode,
Headers headers,
String message,
Throwable cause
);
}Common causes:
Example:
import com.anthropic.core.http.StreamResponse;
import com.anthropic.errors.SseException;
import com.anthropic.models.messages.RawMessageStreamEvent;
try (StreamResponse<RawMessageStreamEvent> stream =
client.messages().createStreaming(params)) {
stream.stream().forEach(event -> {
// Process streaming events
System.out.println(event);
});
} catch (SseException e) {
System.err.println("Streaming error: " + e.getMessage());
// Handle interrupted stream, may need to retry
}Represents network and I/O errors during communication with the API:
package com.anthropic.errors;
public class AnthropicIoException extends AnthropicException {
public AnthropicIoException(String message);
public AnthropicIoException(String message, Throwable cause);
}Common causes:
Example:
import com.anthropic.errors.AnthropicIoException;
try {
Message message = client.messages().create(params);
} catch (AnthropicIoException e) {
System.err.println("I/O error: " + e.getMessage());
// Check network connectivity
// Verify proxy settings
// Consider retry with backoff
}Indicates a failure that could be retried by the client:
package com.anthropic.errors;
public class AnthropicRetryableException extends AnthropicException {
public AnthropicRetryableException(String message);
public AnthropicRetryableException(String message, Throwable cause);
}When thrown:
Example:
import com.anthropic.errors.AnthropicRetryableException;
try {
Message message = client.messages().create(params);
} catch (AnthropicRetryableException e) {
System.err.println("Retryable error: " + e.getMessage());
// Implement custom retry logic
}Thrown when successfully parsed data cannot be interpreted correctly:
package com.anthropic.errors;
public class AnthropicInvalidDataException extends AnthropicException {
public AnthropicInvalidDataException(String message);
public AnthropicInvalidDataException(String message, Throwable cause);
}Common causes:
Example:
import com.anthropic.errors.AnthropicInvalidDataException;
try {
Message message = client.messages().create(params);
// Accessing required property that API unexpectedly omitted
String id = message.id();
} catch (AnthropicInvalidDataException e) {
System.err.println("Invalid data: " + e.getMessage());
// API returned unexpected data format
// May need SDK update
}All service exceptions provide access to response metadata:
import com.anthropic.errors.AnthropicServiceException;
try {
client.messages().create(params);
} catch (AnthropicServiceException e) {
int statusCode = e.statusCode();
System.err.println("HTTP status: " + statusCode);
}Access response headers for additional error context:
import com.anthropic.core.http.Headers;
import com.anthropic.errors.AnthropicServiceException;
try {
client.messages().create(params);
} catch (AnthropicServiceException e) {
Headers headers = e.headers();
// Get specific header
headers.get("x-error-type").stream()
.findFirst()
.ifPresent(type -> System.err.println("Error type: " + type));
// Iterate all headers
headers.names().forEach(name -> {
headers.get(name).forEach(value -> {
System.err.println(name + ": " + value);
});
});
}Request IDs are crucial for debugging. They can be obtained from raw responses or error headers:
import com.anthropic.core.http.HttpResponseFor;
import com.anthropic.models.messages.Message;
import java.util.Optional;
HttpResponseFor<Message> response = client.messages()
.withRawResponse()
.create(params);
Optional<String> requestId = response.requestId();
requestId.ifPresent(id ->
System.out.println("Request ID: " + id)
);import com.anthropic.errors.AnthropicServiceException;
import java.util.Optional;
try {
client.messages().create(params);
} catch (AnthropicServiceException e) {
Optional<String> requestId = e.headers()
.get("request-id")
.stream()
.findFirst();
if (requestId.isPresent()) {
System.err.println("Request ID for debugging: " + requestId.get());
// Include this in bug reports to Anthropic
}
}The SDK automatically retries certain errors with exponential backoff:
The following error types trigger automatic retries (default: 2 retries):
Set custom retry behavior at the client level:
import com.anthropic.client.AnthropicClient;
import com.anthropic.client.okhttp.AnthropicOkHttpClient;
AnthropicClient client = AnthropicOkHttpClient.builder()
.fromEnv()
.maxRetries(4) // Increase from default 2
.build();Disable retries:
AnthropicClient client = AnthropicOkHttpClient.builder()
.fromEnv()
.maxRetries(0) // Disable automatic retries
.build();These errors are NOT automatically retried:
import com.anthropic.errors.AnthropicException;
import com.anthropic.models.messages.Message;
try {
Message message = client.messages().create(params);
System.out.println("Success: " + message.id());
} catch (AnthropicException e) {
System.err.println("Error: " + e.getMessage());
e.printStackTrace();
}import com.anthropic.errors.*;
try {
Message message = client.messages().create(params);
processMessage(message);
} catch (UnauthorizedException e) {
System.err.println("Authentication failed. Check API key.");
// Notify administrator
} catch (RateLimitException e) {
System.err.println("Rate limit exceeded. Backing off...");
// Implement backoff strategy
} catch (InternalServerException e) {
System.err.println("Server error. Will retry.");
// Implement retry logic
} catch (AnthropicIoException e) {
System.err.println("Network error: " + e.getMessage());
// Check connectivity
} catch (AnthropicException e) {
System.err.println("Unexpected error: " + e.getMessage());
// Log for investigation
}import com.anthropic.errors.AnthropicException;
import java.util.concurrent.CompletableFuture;
CompletableFuture<Message> future = client.async().messages().create(params);
future.handle((message, error) -> {
if (error != null) {
if (error instanceof RateLimitException) {
System.err.println("Rate limited");
} else if (error instanceof AnthropicIoException) {
System.err.println("Network error");
} else {
System.err.println("Error: " + error.getMessage());
}
return null;
}
// Process successful message
System.out.println("Success: " + message.id());
return message;
});import com.anthropic.core.http.StreamResponse;
import com.anthropic.errors.SseException;
import com.anthropic.models.messages.RawMessageStreamEvent;
try (StreamResponse<RawMessageStreamEvent> stream =
client.messages().createStreaming(params)) {
stream.stream().forEach(event -> {
try {
processEvent(event);
} catch (Exception e) {
System.err.println("Error processing event: " + e.getMessage());
// Continue processing other events
}
});
} catch (SseException e) {
System.err.println("Streaming interrupted: " + e.getMessage());
// Attempt to resume or restart stream
} catch (AnthropicException e) {
System.err.println("Error during streaming: " + e.getMessage());
}import com.anthropic.errors.*;
import com.anthropic.models.messages.Message;
import java.time.Duration;
public Message createWithRetry(MessageCreateParams params, int maxAttempts) {
int attempt = 0;
long backoffMs = 1000; // Start with 1 second
while (attempt < maxAttempts) {
try {
return client.messages().create(params);
} catch (RateLimitException e) {
attempt++;
if (attempt >= maxAttempts) {
throw e;
}
System.err.println("Rate limited, retrying in " + backoffMs + "ms");
sleep(backoffMs);
backoffMs *= 2; // Exponential backoff
} catch (InternalServerException e) {
attempt++;
if (attempt >= maxAttempts) {
throw e;
}
System.err.println("Server error, retrying in " + backoffMs + "ms");
sleep(backoffMs);
backoffMs *= 2;
} catch (AnthropicIoException e) {
attempt++;
if (attempt >= maxAttempts) {
throw e;
}
System.err.println("Network error, retrying in " + backoffMs + "ms");
sleep(backoffMs);
backoffMs *= 2;
}
}
throw new RuntimeException("Max retry attempts exceeded");
}
private void sleep(long ms) {
try {
Thread.sleep(ms);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
}import com.anthropic.errors.*;
import com.anthropic.models.messages.Message;
import java.time.Instant;
import java.util.concurrent.atomic.AtomicInteger;
public class CircuitBreaker {
private final int failureThreshold;
private final long cooldownMs;
private final AtomicInteger failures = new AtomicInteger(0);
private volatile Instant lastFailureTime;
private volatile boolean open = false;
public CircuitBreaker(int failureThreshold, long cooldownMs) {
this.failureThreshold = failureThreshold;
this.cooldownMs = cooldownMs;
}
public Message create(MessageCreateParams params) {
if (open) {
if (Instant.now().toEpochMilli() - lastFailureTime.toEpochMilli()
> cooldownMs) {
// Try to close circuit
open = false;
failures.set(0);
} else {
throw new RuntimeException("Circuit breaker is open");
}
}
try {
Message message = client.messages().create(params);
failures.set(0); // Reset on success
return message;
} catch (RateLimitException | InternalServerException |
AnthropicIoException e) {
int count = failures.incrementAndGet();
lastFailureTime = Instant.now();
if (count >= failureThreshold) {
open = true;
System.err.println("Circuit breaker opened after " + count +
" failures");
}
throw e;
}
}
}Always log request IDs for failed requests:
import com.anthropic.errors.AnthropicServiceException;
try {
client.messages().create(params);
} catch (AnthropicServiceException e) {
String requestId = e.headers().get("request-id")
.stream().findFirst().orElse("unknown");
System.err.println("Request failed: " + e.getMessage());
System.err.println("Request ID: " + requestId);
System.err.println("Status code: " + e.statusCode());
// Log to monitoring system
logger.error("Anthropic API error",
"requestId", requestId,
"statusCode", e.statusCode(),
"message", e.getMessage());
}Track error rates and types:
import com.anthropic.errors.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
public class ErrorMonitor {
private final ConcurrentHashMap<String, AtomicLong> errorCounts
= new ConcurrentHashMap<>();
public void recordError(AnthropicException e) {
String errorType = e.getClass().getSimpleName();
errorCounts.computeIfAbsent(errorType, k -> new AtomicLong())
.incrementAndGet();
}
public void printStats() {
System.out.println("Error Statistics:");
errorCounts.forEach((type, count) -> {
System.out.println(type + ": " + count.get());
});
}
}import com.anthropic.errors.RateLimitException;
try {
return client.messages().create(params);
} catch (RateLimitException e) {
// Extract retry-after header
String retryAfter = e.headers().get("retry-after")
.stream().findFirst().orElse("60");
long waitSeconds = Long.parseLong(retryAfter);
System.out.println("Rate limited. Waiting " + waitSeconds + " seconds");
// Queue for later or use rate limiter
scheduleRetry(params, waitSeconds);
return null;
}import com.anthropic.errors.*;
public Message createMessageWithFallback(MessageCreateParams params) {
try {
return client.messages().create(params);
} catch (RateLimitException e) {
// Queue for later processing
queueForLater(params);
return createPlaceholderMessage("Queued due to rate limit");
} catch (InternalServerException e) {
// Use cached response if available
return getCachedResponse(params)
.orElseGet(() -> createPlaceholderMessage("Server error"));
} catch (AnthropicException e) {
// Log and return error response
logError(e);
return createErrorMessage(e.getMessage());
}
}import com.anthropic.errors.*;
public boolean isRetryable(AnthropicException e) {
return e instanceof RateLimitException
|| e instanceof InternalServerException
|| e instanceof AnthropicIoException
|| e instanceof AnthropicRetryableException;
}
public Message createWithConditionalRetry(MessageCreateParams params) {
try {
return client.messages().create(params);
} catch (AnthropicException e) {
if (isRetryable(e)) {
// Retry logic
return retryCreate(params);
} else {
// Don't retry, handle error
System.err.println("Non-retryable error: " + e.getMessage());
throw e;
}
}
}Enable response validation to catch data issues early:
import com.anthropic.core.RequestOptions;
import com.anthropic.models.messages.Message;
// Per-request validation
Message message = client.messages().create(
params,
RequestOptions.builder()
.responseValidation(true)
.build()
);
// Or configure globally
AnthropicClient client = AnthropicOkHttpClient.builder()
.fromEnv()
.responseValidation(true)
.build();import com.anthropic.core.http.StreamResponse;
import com.anthropic.errors.SseException;
import com.anthropic.helpers.MessageAccumulator;
public Message createWithStreamRecovery(MessageCreateParams params) {
MessageAccumulator accumulator = MessageAccumulator.create();
try (StreamResponse<RawMessageStreamEvent> stream =
client.messages().createStreaming(params)) {
stream.stream()
.peek(accumulator::accumulate)
.forEach(this::processEvent);
return accumulator.message();
} catch (SseException e) {
System.err.println("Stream interrupted: " + e.getMessage());
// Return partial message if useful
if (accumulator.message() != null) {
System.out.println("Returning partial message");
return accumulator.message();
}
throw e;
}
}Always close clients and streams properly:
import com.anthropic.client.AnthropicClient;
import com.anthropic.core.http.StreamResponse;
// Close client when done
try (AnthropicClient client = AnthropicOkHttpClient.fromEnv()) {
// Use client
} // Automatically closed
// Close streams
try (StreamResponse<RawMessageStreamEvent> stream =
client.messages().createStreaming(params)) {
stream.stream().forEach(this::processEvent);
} // Automatically closedAdd application context to error messages:
import com.anthropic.errors.AnthropicException;
public void processUserRequest(String userId, String requestText) {
try {
MessageCreateParams params = MessageCreateParams.builder()
.addUserMessage(requestText)
.model(Model.CLAUDE_SONNET_4_20250514)
.maxTokens(1024L)
.build();
Message message = client.messages().create(params);
saveResponse(userId, message);
} catch (AnthropicException e) {
// Include application context
System.err.println("Failed to process request for user: " + userId);
System.err.println("Request text length: " + requestText.length());
System.err.println("Error: " + e.getMessage());
if (e instanceof AnthropicServiceException) {
AnthropicServiceException se = (AnthropicServiceException) e;
System.err.println("Status: " + se.statusCode());
String requestId = se.headers().get("request-id")
.stream().findFirst().orElse("unknown");
System.err.println("Request ID: " + requestId);
}
throw new RuntimeException("Failed to process request for user " + userId, e);
}
}The Anthropic Java SDK provides comprehensive error handling through a well-structured exception hierarchy:
All exceptions provide rich context including status codes, headers, and request IDs for debugging. The SDK automatically retries appropriate errors with exponential backoff, while allowing customization of retry behavior.
Following best practices like logging request IDs, implementing monitoring, handling rate limits gracefully, and using appropriate retry strategies will help build robust applications with the Anthropic SDK.
Install with Tessl CLI
npx tessl i tessl/maven-com-anthropic--anthropic-java