boto3
error handling
AWS SDK
Python
exceptions

Properly catch boto3 Errors

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

With boto3, the safest way to handle AWS failures is to catch the right exception class and then inspect the structured error code that AWS returned. Many examples online either catch Exception too broadly or compare human-readable messages that can change. Robust boto3 code should branch on documented exception types and machine-readable error codes instead.

Catch ClientError for Service Responses

Most AWS API failures that come back from a service are raised as botocore.exceptions.ClientError.

python
1import boto3
2from botocore.exceptions import ClientError
3
4s3 = boto3.client("s3")
5
6try:
7    s3.head_bucket(Bucket="example-bucket")
8except ClientError as err:
9    code = err.response["Error"]["Code"]
10    print(f"AWS returned error code: {code}")

The important data is in err.response, especially the nested Error entry. That structure is much more stable than the formatted exception string.

Separate SDK Problems from Service Problems

Not every boto3 failure is a service response. Some errors come from the SDK or transport layer itself.

python
1from botocore.exceptions import BotoCoreError, ClientError
2
3try:
4    boto3.client("s3").list_buckets()
5except ClientError as err:
6    print("Service rejected the request:", err.response["Error"]["Code"])
7except BotoCoreError as err:
8    print("SDK or network-level failure:", str(err))

This split matters because retry logic and user feedback may differ. A throttling response from AWS is not the same kind of problem as a broken credential provider chain or a connection failure.

Branch on Error Codes, Not Full Messages

When you need special handling, inspect Error.Code.

python
1import boto3
2from botocore.exceptions import ClientError
3
4s3 = boto3.client("s3")
5
6try:
7    s3.get_object(Bucket="example-bucket", Key="missing.txt")
8except ClientError as err:
9    code = err.response["Error"]["Code"]
10    if code in ("NoSuchKey", "404"):
11        print("Object does not exist")
12    elif code == "AccessDenied":
13        print("Permissions are insufficient")
14    else:
15        raise

Re-raising unknown cases is important. Otherwise, broad exception handling can hide real production failures.

Prefer Specific Service Exceptions When Available

Some clients expose service-specific exception helpers.

python
1import boto3
2
3s3 = boto3.client("s3")
4
5try:
6    s3.get_object(Bucket="example-bucket", Key="missing.txt")
7except s3.exceptions.NoSuchKey:
8    print("Object does not exist")

This can improve readability, but not every case has a clean generated exception class. ClientError with code inspection remains the most generally reliable pattern.

Log Enough Context to Debug

Error handling is not only about catching. You also want enough context to debug without logging secrets. That usually means the AWS operation, the resource identifier, and the error code.

A good handler is therefore selective and observable. It decides which failures are expected, which deserve retries, and which should terminate the workflow.

Add Retry Logic Deliberately

Some boto3 failures are transient and should be retried, while others are permanent. Throttling, temporary network failures, and timeouts often deserve another attempt. AccessDenied, malformed requests, and missing required parameters usually do not.

That means exception handling and retry policy should be designed together. Catching an error is only the first step. You still need to decide whether the call should be retried, surfaced to a caller, or turned into a domain-specific application error.

Test the Failure Path

Error handling code is easy to leave untested because the success path is simpler to reproduce. With boto3, it is worth exercising the unhappy path explicitly so you know your code branches on real AWS-style responses.

python
1import boto3
2from botocore.stub import Stubber
3from botocore.exceptions import ClientError
4
5s3 = boto3.client("s3")
6stubber = Stubber(s3)
7stubber.add_client_error("head_bucket", service_error_code="404", service_message="Not Found")
8
9with stubber:
10    try:
11        s3.head_bucket(Bucket="missing-bucket")
12    except ClientError as err:
13        print(err.response["Error"]["Code"])

This kind of test confirms that your handler is reading structured fields rather than relying on assumptions about message formatting.

Common Pitfalls

  • Catching plain Exception and losing the distinction between AWS failures and unrelated bugs.
  • Matching on the formatted error message instead of the structured error code.
  • Swallowing unknown ClientError cases instead of re-raising them.
  • Treating SDK failures and service responses as if they were the same category.
  • Logging credentials or entire request payloads while debugging AWS errors.

Summary

  • Catch ClientError for AWS service-side failures.
  • Catch BotoCoreError separately for SDK and transport problems.
  • Use err.response["Error"]["Code"] for decision-making.
  • Re-raise unexpected cases instead of hiding them.
  • Log resource and operation context, but avoid leaking secrets.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.