Skip to main content

Error Handling

The SDK provides a typed exception hierarchy so you can catch exactly the errors you care about and let the rest propagate.


Exception hierarchy

API errors are reported through WatsonxException, which extends RuntimeException. You never need to declare SDK exceptions in throws clauses.

RuntimeException
├── WatsonxException (base - always has statusCode, errorCode, message, traceId)
│ ├── AuthenticationTokenExpiredException ← handled automatically by the SDK
│ ├── AuthorizationRejectedException
│ ├── InvalidInputArgumentException
│ ├── InvalidRequestEntityException
│ ├── JsonTypeErrorException
│ ├── JsonValidationErrorException
│ ├── ModelNotSupportedException
│ ├── ModelNoSupportForFunctionException
│ ├── TokenQuotaReachedException
│ └── UserAuthorizationFailedException
├── EmptyChatResponseException ← the call succeeded, but there is nothing to read
└── ModerationException ← the call succeeded, but moderation blocked the output

Both chat exceptions live in com.ibm.watsonx.ai.chat.exception.

WatsonxException exposes:

MethodTypeDescription
statusCode()intHTTP status code (e.g. 400, 429, 500)
errorCode()StringMachine-readable error code from the API (e.g. "token_quota_reached")
getMessage()StringHuman-readable error description
traceId()StringIBM trace ID - include this when reporting issues

If the API returns an error code that does not map to a specific subclass, the base WatsonxException is thrown.


Specific exception types

ExceptionHTTPError codeWhen it occurs
AuthenticationTokenExpiredException401authentication_token_expiredToken expired mid-request (SDK retries automatically)
AuthorizationRejectedException403authorization_rejectedAPI key lacks permission for this operation
InvalidInputArgumentException400invalid_input_argumentA request parameter has an invalid value
InvalidRequestEntityException400invalid_request_entityThe request body is malformed or violates constraints
JsonTypeErrorException400json_type_errorA JSON field has the wrong type
JsonValidationErrorException400json_validation_errorJSON schema validation failed
ModelNotSupportedException400model_not_supportedThe requested model ID is not available in this region or plan
ModelNoSupportForFunctionException400model_no_support_for_functionThe model does not support the requested capability (e.g. tool calling)
TokenQuotaReachedException429token_quota_reachedToken quota for the account or project has been reached
UserAuthorizationFailedException403user_authorization_failedUser-level authorization check failed

Automatic token refresh: AuthenticationTokenExpiredException is caught internally by the SDK, which refreshes the token and retries the request. You will only see this exception if the retry also fails. The retry limit is configurable via WATSONX_RETRY_TOKEN_EXPIRED_MAX_RETRIES. See Environment Variables.


Empty chat responses

EmptyChatResponseException is not an API error. The request succeeded, but the response carries nothing that can be turned into an AssistantMessage, so the message cannot be built. It is thrown by ChatResponse.toAssistantMessage() and ChatResponse.toAssistantMessages() when:

  • the response contains no choices at all
  • a choice carries no message
  • a choice has no content, no tool calls and no refusal, for example when the model was truncated by maxCompletionTokens before emitting anything

Because it extends RuntimeException directly and not WatsonxException, a catch (WatsonxException e) block will not catch it.

EmptyChatResponseException exposes:

MethodTypeDescription
finishReason()FinishReasonFinish reason of the empty choice (e.g. LENGTH, TIME_LIMIT, CANCELLED, ERROR), or INCOMPLETE when the response has no choices
index()intZero-based index of the empty choice, or EmptyChatResponseException.NO_CHOICE (-1) when the response has no choices
response()ChatResponseThe original response - useful to inspect token usage or log the raw payload

Moderation-blocked responses

ModerationException is thrown when the moderation system blocked the response entirely, leaving no choices in the output. Like EmptyChatResponseException, it is raised by ChatResponse.toAssistantMessage() and toAssistantMessages() rather than by the request itself, and it extends RuntimeException directly, so catch (WatsonxException e) will not catch it. In streaming mode the returned CompletableFuture completes exceptionally with it and ChatHandler.onError receives it.

ModerationException exposes:

MethodTypeDescription
moderations()Map<String, List<ModerationResult>>Unmodifiable map from detector name ("pii", "hap", "granite_guardian") to the flagged spans, never null
getMessage()StringLists the policies that were triggered

To decide without catching an exception, TextChatResponse.isBlockedByModeration() reports the same condition on the response object. See Content Moderation for the detector configuration.


Usage examples

Basic error handling

Catch specific subclasses first, then fall back to WatsonxException for anything else:

try {
ChatResponse response = chatService.chat("Hello!");
} catch (TokenQuotaReachedException e) {
// Quota exceeded - back off and retry later, or switch to a different project
handleQuotaExceeded();
} catch (ModelNotSupportedException e) {
// The configured modelId is not available - switch to an alternative
useAlternativeModel();
} catch (InvalidInputArgumentException e) {
// A parameter value is wrong - log for debugging
logger.error("Bad request: {} (traceId={})", e.getMessage(), e.traceId());
} catch (WatsonxException e) {
// Any other API error
logger.error("Watsonx error [{}] {}: {} (traceId={})",
e.statusCode(), e.errorCode(), e.getMessage(), e.traceId());
}

Handling an empty chat response

EmptyChatResponseException is raised when the assistant message is built, not when the request is sent, so catch it around the conversion:

ChatResponse response = chatService.chat("Hello!");

try {
AssistantMessage message = response.toAssistantMessage();
System.out.println(message.content());
} catch (EmptyChatResponseException e) {
switch (e.finishReason()) {
// Truncated before producing output - raise maxCompletionTokens and retry
case LENGTH -> retryWithLargerBudget();
// The request hit the server time limit or was cancelled
case TIME_LIMIT, CANCELLED -> retryLater();
default -> logger.warn("Empty choice {} ({}): {}",
e.index(), e.finishReason(), e.getMessage());
}
}

Handling transient errors

The SDK automatically retries 429, 502, 503, 504, and 520 responses with exponential backoff (see Environment Variables). If retries are exhausted, WatsonxException is thrown with the final status code. To implement your own retry on top:

int attempts = 0;
while (attempts < 3) {
try {
return chatService.chat("Hello!");
} catch (WatsonxException e) {
if (e.statusCode() == 503 && attempts < 2) {
attempts++;
Thread.sleep(1000L * attempts);
} else {
throw e;
}
}
}