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
AccessDenied/403: the IAM policy (or bucket policy, or KMS key) does not allow the action. With S3, listing permission is needed to get 404 instead of 403 for a missing key.NoSuchKey,NoSuchBucket,ResourceNotFoundException(DynamoDB, Lambda): wrong name or wrong region.ThrottlingException,SlowDown,ProvisionedThroughputExceededException: retry with backoff (botocore's retry config:Config(retries={"mode": "adaptive"})).ExpiredToken,InvalidClientTokenId,SignatureDoesNotMatch: credentials problem; see boto3 Unable to locate credentials.
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.