AWS Developer Tools Blog

New Additions to Exception Handling

Exception handling in the AWS SDK for Java just got a little easier! We just introduced base exception classes for all AWS services in the SDK. Now all modeled exceptions extend from this service-specific base exception and all unmodeled exceptions (unknown exceptions thrown by the service) will be unmarshalled into the service-specific base exception, and not into the generic AmazonServiceException. The new service base exceptions still extend AmazonServiceException, so existing exception handling code you’ve written won’t be affected. This change gives you greater control to handle exceptions differently, depending on the originating service.

Writing robust applications is challenging, and distributed systems occasionally fail with transient (often retriable) errors. Although the SDK retries certain errors by default, sometimes these exceptions bubble up to the caller’s code and must be handled appropriately. Previously, when you used the SDK, you only had the option to catch a service-specific modeled exception (i.e., ResourceInUseException for Amazon DynamoDB) or a generic AmazonServiceException, which could be thrown by any service. When you wrote code that involved multiple service dependencies, you had to write error handling code for each service independently.

Consider the following example where you read messages from an Amazon SQS queue and write each message to a DynamoDB table. The contrived requirements are to treat SQS errors as transient and retry them after an appropriate backoff time, and to treat all DynamoDB errors (except for ProvisionedThroughputExceededException) as fatal and persist the messages to local disk for future processing. ProvisionedThroughputExceededException should be treated as retriable and the program should back off before attempting to process another message. Finally, if any other non-service exceptions are thrown, the program should terminate. For simplicity, we’ll assume message processing is idempotent and it’s acceptable to process a message multiple times.

Previously, this was difficult to do in the SDK. Your options included catching service specific modeled exceptions (like ProvisionedThroughputExceededException) or catching the generic AmazonServiceException which applies to all AWS services. This makes handling errors differently per service challenging. You might have written code like this to meet the example’s requirements.


while (true) {
    ReceiveMessageResult currentResult = null;
    try {
        // Receive messages from the queue
        currentResult = sqs.receiveMessage(QUEUE_URL);
        for (Message message : currentResult.getMessages()) {
            // Save message to DynamoDB
            ddb.putItem(new PutItemRequest().withTableName(TABLE_NAME).withItem(
                 ImmutableMapParameter
                   .of("messageId", new AttributeValue(message.getMessageId()),
                       "messageBody", new AttributeValue(message.getBody()))));
            // Delete message from queue to indicate it's been processed
            sqs.deleteMessage(new DeleteMessageRequest()
                                    .withQueueUrl(QUEUE_URL)
                                    .withReceiptHandle(message.getReceiptHandle()));
        }
    } catch (ProvisionedThroughputExceededException e) {
        LOG.warn("Table capacity exceeding. Backing off and retrying.");
        Thread.sleep(1000);
    } catch (AmazonServiceException e) {
        switch (e.getServiceName()) {
            // Have to use magic service name constants that aren't publicly exposed
            case "AmazonSQS":
                LOG.warn("SQS temporarily unavailable. Backing off and retrying.");
                Thread.sleep(1000);
                break;
            case "AmazonDynamoDBv2":
                LOG.fatal("Could not save messages to DynamoDB. Saving to disk.");
                saveToFile(currentResult.getMessages());
                break;
        }
    } catch (AmazonClientException e) {
        // Caught unexpected error
        System.exit(1);
    }
}

You can rewrite the example above much more cleanly by using the new exception base classes in the SDK, as shown here.
 


while (true) {
    ReceiveMessageResult currentResult = null;
    try {
        // Receive messages from the queue
        currentResult = sqs.receiveMessage(QUEUE_URL);
        for (Message message : currentResult.getMessages()) {
            // Save message to DynamoDB
            ddb.putItem(new PutItemRequest().withTableName(TABLE_NAME).withItem(
                 ImmutableMapParameter
                   .of("messageId", new AttributeValue(message.getMessageId()),
                       "messageBody", new AttributeValue(message.getBody()))));
            // Delete message from queue to indicate it's been processed
            sqs.deleteMessage(new DeleteMessageRequest()
                                    .withQueueUrl(QUEUE_URL)
                                    .withReceiptHandle(message.getReceiptHandle()));
        }
    } catch (AmazonSQSException e) {
        LOG.warn("SQS temporarily unavailable. Backing off and retrying.");
        Thread.sleep(1000);
    } catch (ProvisionedThroughputExceededException e) {
        // You can catch modeled exceptions
        LOG.warn("Table capacity exceeding. Backing off and retrying.");
        Thread.sleep(1000);
    } catch (AmazonDynamoDBException e) {
        // Or you can catch the service base exception to handle any exception that
        // can be thrown by a particular service
        LOG.fatal("Could not save messages to DynamoDB. Saving to file.");
        saveToFile(currentResult.getMessages());
    } catch (AmazonClientException e) {
        // Caught unexpected error
        System.exit(1);
    }
}

We hope this addition to the SDK makes writing robust applications even easier! Let us know what you think in the comments section below.