boto3 ClientError error codes and handling (tested)

boto3 raises botocore.exceptions.ClientError for every AWS service error. Branch on e.response["Error"]["Code"] (e.g. NoSuchKey, AccessDenied) or catch the client's modeled classes like s3.exceptions.NoSuchKey, and re-raise what you don't expect.

1. Read the error code, not the message

import boto3
from botocore.exceptions import ClientError
from botocore.stub import Stubber
s3 = boto3.client("s3", region_name="us-east-1", aws_access_key_id="x", aws_secret_access_key="x")
stub = Stubber(s3)
stub.add_client_error("head_object", service_error_code="404", service_message="Not Found", http_status_code=404)
stub.add_client_error("get_object", service_error_code="NoSuchKey", service_message="The specified key does not exist.", http_status_code=404)
stub.add_client_error("list_objects_v2", service_error_code="AccessDenied", service_message="Access Denied", http_status_code=403)
with stub:
    for call in (lambda: s3.head_object(Bucket="b", Key="k"),
                 lambda: s3.get_object(Bucket="b", Key="k"),
                 lambda: s3.list_objects_v2(Bucket="b")):
        try:
            call()
        except ClientError as e:
            err = e.response["Error"]
            print(f"{err['Code']:13} HTTP {e.response['ResponseMetadata']['HTTPStatusCode']} | {e}")

Output:

404           HTTP 404 | An error occurred (404) when calling the HeadObject operation: Not Found
NoSuchKey     HTTP 404 | An error occurred (NoSuchKey) when calling the GetObject operation: The specified key does not exist.
AccessDenied  HTTP 403 | An error occurred (AccessDenied) when calling the ListObjectsV2 operation: Access Denied

Every AWS service error reaches boto3 as botocore.exceptions.ClientError. The machine-readable parts are in e.response["Error"]["Code"] and e.response["ResponseMetadata"]["HTTPStatusCode"]; the printed text An error occurred (Code) when calling the X operation: message is built from them. Branch on the code, never on the message text.

Watch out for HeadObject: a HEAD response has no body, so S3 cannot send a named code and you get the bare status ("404", "403") instead of NoSuchKey. Code that checks == "NoSuchKey" after head_object never matches; check for "404" there.

2. Catch modeled exceptions, re-raise the rest

import boto3
from botocore.exceptions import ClientError
from botocore.stub import Stubber
s3 = boto3.client("s3", region_name="us-east-1", aws_access_key_id="x", aws_secret_access_key="x")
stub = Stubber(s3)
stub.add_client_error("get_object", service_error_code="NoSuchKey", http_status_code=404)
stub.add_client_error("get_object", service_error_code="AccessDenied", http_status_code=403)
def read(key):
    try:
        return s3.get_object(Bucket="b", Key=key)["Body"].read()
    except s3.exceptions.NoSuchKey:            # modeled exception class
        return None
    except ClientError as e:
        if e.response["Error"]["Code"] == "AccessDenied":
            print("re-raising:", e.response["Error"]["Code"])
        raise
with stub:
    print("missing key ->", read("a"))
    print("NoSuchKey is a ClientError:", issubclass(s3.exceptions.NoSuchKey, ClientError))
    try:
        read("b")
    except ClientError as e:
        print("caller got", type(e).__name__)

Output:

missing key -> None
NoSuchKey is a ClientError: True
re-raising: AccessDenied
caller got AccessDenied

Errors that a service declares in its API model are exposed as classes on the client, e.g. s3.exceptions.NoSuchKey. They are subclasses of ClientError, so catch the specific one first and a general ClientError after it. In the last line the class name follows the error code (AccessDenied) but it is still a ClientError. Re-raise anything you do not expect rather than swallowing it: an AccessDenied is a permissions bug to fix, not a missing file.

Common codes

Testing error paths without AWS

Both blocks use botocore.stub.Stubber, which makes the client return a canned error with the code and status you choose; no network or account needed. That is the easiest way to unit-test your except branches.

Frequently asked questions

How do I get the error code from a boto3 ClientError?

Use e.response['Error']['Code']; the HTTP status is in e.response['ResponseMetadata']['HTTPStatusCode'].

Why does head_object return 404 instead of NoSuchKey?

HEAD responses have no body, so S3 cannot send a named error code; boto3 reports the status code '404' as the code.

How do I catch NoSuchKey in boto3?

except s3.exceptions.NoSuchKey: on the client you used. It is a subclass of ClientError, so put it before a general except ClientError.

Written with AI assistance. Measured on 2026-10-23 with boto3/botocore 1.43.118 and Python 3.14.7 (python:3-alpine in Docker). Errors are canned with botocore Stubber (no AWS calls); each block was run verbatim and the output pasted unedited. PlainHeaders is our extension; Python and boto3 are free.