boto3 icon indicating copy to clipboard operation
boto3 copied to clipboard

"Error handling" docs are right for S3 but wrong for SES

Open anentropic opened this issue 7 months ago • 2 comments

Describe the issue

The docs say:

Parsing for error responses uses the same exact methodology outlined in the low-level client section. Catching exceptions through the client’s exceptions property is slightly different, as you’ll need to access the client’s meta property to get to the exceptions.

client.meta.client.exceptions.SomeServiceException

Using Amazon S3 as an example resource service, you can use the client’s exception property to catch the BucketAlreadyExists exception. And you can still parse the error response to get the bucket name that’s passed in the original request.

import botocore
import boto3

client = boto3.resource('s3')

try:
    client.create_bucket(BucketName='amzn-s3-demo-bucket')

except client.meta.client.exceptions.BucketAlreadyExists as err:
    print("Bucket {} already exists!".format(err.response['Error']['BucketName']))
    raise err

This works for S3 client. But not for SES client:

In [26]: ses = boto3.client("ses")

In [27]: ses.meta.client.exceptions
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[27], line 1
----> 1 ses.meta.client.exceptions

AttributeError: 'ClientMeta' object has no attribute 'client'

For SES client I can do:

In [28]: ses.exceptions
Out[28]: <botocore.errorfactory.SESExceptions at 0x125410fd0>

But that doesn't work for S3:

In [30]: client = boto3.resource('s3')
    ...:

In [32]: client.exceptions
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[32], line 1
----> 1 client.exceptions

AttributeError: 's3.ServiceResource' object has no attribute 'exceptions'

So it seems that the different clients are inconsistent, and the docs need to reflect this.

Links

https://boto3.amazonaws.com/v1/documentation/api/latest/guide/error-handling.html

I'm using boto3==1.38.17 on Python 3.11

anentropic avatar May 19 '25 08:05 anentropic

Hello @anentropic, thanks for reaching out. There are two ways to interact to AWS services using boto, its either Client or Resources (Client: https://boto3.amazonaws.com/v1/documentation/api/latest/guide/clients.html , Resources: https://boto3.amazonaws.com/v1/documentation/api/latest/guide/resources.html ) . For handling errors and as shown by the documentation https://boto3.amazonaws.com/v1/documentation/api/latest/guide/error-handling.html#error-handling , it shows how to handle client errors and resources errors.

If you are using a client, then you must use a client error handing. If you are using a resource, then you must use a client error handling. Below shows an example:

import boto3
from botocore.exceptions import ClientError

ses = boto3.client('ses')

try:
    # Use an invalid email address to trigger an exception
    ses.verify_email_identity(EmailAddress='not-a-valid-email')
    print("Operation succeeded")
    
except ses.exceptions.MessageRejected as e:
    print(f"Message was rejected: {e}")
    
except ses.exceptions.InvalidParameterValue as e:
    print(f"Invalid parameter: {e}")
    
except ClientError as e:
    print(f"Other error occurred: {e.response['Error']['Code']}: {e.response['Error']['Message']}")

OUTPUT:
Traceback (most recent call last):
  File "/Users/x/github_issues/4534/examples.py", line 8, in <module>
    ses.verify_email_identity(EmailAddress='not-a-valid-email')
    ~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/x/.venv/lib/python3.13/site-packages/botocore/client.py", line 598, in _api_call
    return self._make_api_call(operation_name, kwargs)
           ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/x/.venv/lib/python3.13/site-packages/botocore/context.py", line 123, in wrapper
    return func(*args, **kwargs)
  File "/Users/x/.venv/lib/python3.13/site-packages/botocore/client.py", line 1061, in _make_api_call
    raise error_class(parsed_response, operation_name)
botocore.exceptions.ClientError: An error occurred (InvalidParameterValue) when calling the VerifyEmailIdentity operation: Invalid email address<not-a-valid-email>.

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/Users/x/github_issues/4534/examples.py", line 14, in <module>
    except ses.exceptions.InvalidParameterValue as e:
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/x/.venv/lib/python3.13/site-packages/botocore/errorfactory.py", line 51, in __getattr__
    raise AttributeError(
    ...<2 lines>...
    )
AttributeError: <botocore.errorfactory.SESExceptions object at 0x107315d30> object has no attribute InvalidParameterValue. Valid exceptions are: AccountSendingPausedException, AlreadyExistsException, CannotDeleteException, ConfigurationSetAlreadyExistsException, ConfigurationSetDoesNotExistException, ConfigurationSetSendingPausedException, CustomVerificationEmailInvalidContentException, CustomVerificationEmailTemplateAlreadyExistsException, CustomVerificationEmailTemplateDoesNotExistException, EventDestinationAlreadyExistsException, EventDestinationDoesNotExistException, FromEmailAddressNotVerifiedException, InvalidCloudWatchDestinationException, InvalidConfigurationSetException, InvalidDeliveryOptionsException, InvalidFirehoseDestinationException, InvalidLambdaFunctionException, InvalidPolicyException, InvalidRenderingParameterException, InvalidS3ConfigurationException, InvalidSNSDestinationException, InvalidSnsTopicException, InvalidTemplateException, InvalidTrackingOptionsException, LimitExceededException, MailFromDomainNotVerifiedException, MessageRejected, MissingRenderingAttributeException, ProductionAccessNotGrantedException, RuleDoesNotExistException, RuleSetDoesNotExistException, TemplateDoesNotExistException, TrackingOptionsAlreadyExistsException, TrackingOptionsDoesNotExistException

= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = 

import boto3
from botocore.exceptions import ClientError

# Create an S3 resource
s3 = boto3.resource('s3')

try:
    # Try to get an object that doesn't exist
    obj = s3.Object('my-unique-bucket-name', 'non-existent-key')
    content = obj.get()  # This will fail with NoSuchKey
    
    print(f"Content: {content}")
    
except s3.meta.client.exceptions.NoSuchKey as e:
    print(f"Caught specific exception: Object doesn't exist: {e}")
    
except ClientError as e:
    print(f"Caught generic exception: {e}")
    print(f"Error code: {e.response['Error']['Code']}")

OUTPUT: 

Caught generic exception: An error occurred (AccessDenied) when calling the GetObject operation: Access Denied
Error code: AccessDenied

= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = 

import boto3
from botocore.exceptions import ClientError

# Create an S3 client
s3_client = boto3.client('s3')

try:
    # Try to get an object that doesn't exist
    response = s3_client.get_object(
        Bucket='my-unique-bucket-name',
        Key='non-existent-key'
    )
    
    print(f"Content: {response}")
    
except s3_client.exceptions.NoSuchKey as e:
    print(f"Caught specific exception: Object doesn't exist: {e}")
    
except ClientError as e:
    print(f"Caught generic exception: {e}")
    print(f"Error code: {e.response['Error']['Code']}")

OUTPUT: 
Caught generic exception: An error occurred (AccessDenied) when calling the GetObject operation: Access Denied
Error code: AccessDenied

If using a resource error handling to a client and vice versa, it will not work. As shown below and as what you have tested:

import boto3
from botocore.exceptions import ClientError

s3_client = boto3.client('s3')

try:
    response = s3_client.get_object(
        Bucket='my-unique-bucket-name',
        Key='non-existent-key'
    )
    
except s3_client.meta.client.exceptions.NoSuchKey as e: 
    print(f"Caught specific exception: Object doesn't exist: {e}")
    
except ClientError as e:
    print(f"Caught generic exception: {e}")

OUTPUT:
AttributeError: 'ClientMeta' object has no attribute 'client'

= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = 

import boto3
from botocore.exceptions import ClientError

s3 = boto3.resource('s3')

try:
    obj = s3.Object('my-unique-bucket-name', 'non-existent-key')
    content = obj.get()
    
except s3.exceptions.NoSuchKey as e: 
    print(f"Caught specific exception: Object doesn't exist: {e}")
    
except ClientError as e:
    print(f"Caught generic exception: {e}")

OUTPUT: 
AttributeError: 's3.ServiceResource' object has no attribute 'exceptions'

Please let me know if you have any questions. Thanks.

adev-code avatar Jun 06 '25 05:06 adev-code

@adev-code thanks, I hadn't noticed the client/resource distinction

I guess this is because not all services have resources? SES doesn't seem to have one

and then S3 docs start with showing a client https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3.html but then if you know to look for it and scroll down it's there https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3.html#resources

anentropic avatar Jun 09 '25 09:06 anentropic

Resources were a popular feature that provided abstractions over the low-level client. These were hand written and only done for a select few services like S3 and DynamoDB. Resources are now “feature frozen” and we advice customers to use low level client instead.

adev-code avatar Sep 16 '25 22:09 adev-code

This issue is now closed. Comments on closed issues are hard for our team to see. If you need more assistance, please open a new issue that references this one.

github-actions[bot] avatar Sep 16 '25 22:09 github-actions[bot]