Debugging Basics
Six examples to get you comfortable reading AWS SDK errors - four basic and two intermediate.
Search across all documentation pages
Six examples to get you comfortable reading AWS SDK errors - four basic and two intermediate.
Each example shows the same operation in both Python (boto3) and TypeScript (AWS SDK for JavaScript v3). By the end you can pull the error code, the request id, and enough context to know which playbook in this section to reach for.
pip install boto3 (boto3 1.43.x, Python 3.10+). TypeScript: npm install @aws-sdk/client-dynamodb (Node.js 18+; install the client for whichever service you use).aws configure, environment variables, or an IAM role.ClientError in boto3, a typed service exception in SDK v3.The first move in any AWS SDK debugging session is separating the error code from the message.
# --- Python (boto3) ---
from botocore.exceptions import ClientError
try:
ddb.get_item(TableName="orders", Key={"id": {"S": "123"}})
except ClientError as err:
code = err.response["Error"]["Code"]
message = err.response["Error"]["Message"]
print(f"code={code} message={message}")// --- TypeScript (AWS SDK v3) ---
import { GetItemCommand } from "@aws-sdk/client-dynamodb";
try {
await ddb.send(new GetItemCommand({ TableName: "orders", Key: { id: { S: "123" } } }));
} catch (err) {
const e = err as { name?: string; message?: string };
console.log(`code=${e.name} message=${e.message}`);
}ClientError for every service-side failure; the code lives at err.response["Error"]["Code"]..name property both carry the error code.Related: Categories of AWS SDK Failures - which bucket a code falls into.
Every AWS API response carries a request id, and it is the single most useful thing to log for support and tracing.
# --- Python (boto3) ---
from botocore.exceptions import ClientError
try:
ddb.get_item(TableName="orders", Key={"id": {"S": "123"}})
except ClientError as err:
request_id = err.response["ResponseMetadata"]["RequestId"]
print(f"request_id={request_id}")// --- TypeScript (AWS SDK v3) ---
import { GetItemCommand } from "@aws-sdk/client-dynamodb";
try {
await ddb.send(new GetItemCommand({ TableName: "orders", Key: { id: { S: "123" } } }));
} catch (err) {
const e = err as { $metadata?: { requestId?: string } };
console.log(`request_id=${e.$metadata?.requestId}`);
}ResponseMetadata.RequestId on the ClientError.$metadata.requestId on the thrown exception.The HTTP status code tells you whether the problem is your request or AWS's side.
# --- Python (boto3) ---
from botocore.exceptions import ClientError
try:
ddb.get_item(TableName="orders", Key={"id": {"S": "123"}})
except ClientError as err:
status = err.response["ResponseMetadata"]["HTTPStatusCode"]
if 400 <= status < 500:
print("client-side: fix the request")
else:
print("server-side: likely transient, safe to retry")// --- TypeScript (AWS SDK v3) ---
import { GetItemCommand } from "@aws-sdk/client-dynamodb";
try {
await ddb.send(new GetItemCommand({ TableName: "orders", Key: { id: { S: "123" } } }));
} catch (err) {
const status = (err as { $metadata?: { httpStatusCode?: number } }).$metadata?.httpStatusCode ?? 0;
if (status >= 400 && status < 500) {
console.log("client-side: fix the request");
} else {
console.log("server-side: likely transient, safe to retry");
}
}4xx almost always means fix the request - bad input, missing permission, wrong resource name.5xx usually means a transient service-side condition, which the SDK's retry logic already handles for most calls.ThrottlingException is technically a 4xx but behaves like a transient error - it is retryable, unlike most 4xxs.Both SDKs know, per error, whether a retry is likely to help.
# --- Python (boto3) ---
from botocore.exceptions import ClientError
try:
ddb.get_item(TableName="orders", Key={"id": {"S": "123"}})
except ClientError as err:
retryable_codes = {"ThrottlingException", "ProvisionedThroughputExceededException"}
code = err.response["Error"]["Code"]
print("retryable" if code in retryable_codes else "not retryable")// --- TypeScript (AWS SDK v3) ---
import { GetItemCommand } from "@aws-sdk/client-dynamodb";
try {
await ddb.send(new GetItemCommand({ TableName: "orders", Key: { id: { S: "123" } } }));
} catch (err) {
const e = err as { $retryable?: { throttling?: boolean } };
console.log(e.$retryable ? "retryable" : "not retryable");
}retryable flag on the exception; you check the code against known throttling/transient codes, or trust that the built-in retry strategy already tried and gave up.$retryable hint to some errors, including a throttling flag.catch/except block at all, the SDK's own retry budget has already been exhausted for that call.The request id from your SDK error is the same request id that appears in the service's own CloudWatch Logs, if the service logs per-request.
# --- Python (boto3) ---
import boto3
from botocore.exceptions import ClientError
logs = boto3.client("logs")
try:
ddb.get_item(TableName="orders", Key={"id": {"S": "123"}})
except ClientError as err:
request_id = err.response["ResponseMetadata"]["RequestId"]
# Search the relevant log group for this request id to find server-side context.
logs.filter_log_events(logGroupName="/aws/lambda/orders-handler", filterPattern=request_id)// --- TypeScript (AWS SDK v3) ---
import { CloudWatchLogsClient, FilterLogEventsCommand } from "@aws-sdk/client-cloudwatch-logs";
const logsClient = new CloudWatchLogsClient({});
try {
await ddb.send(new GetItemCommand({ TableName: "orders", Key: { id: { S: "123" } } }));
} catch (err) {
const requestId = (err as { $metadata?: { requestId?: string } }).$metadata?.requestId;
await logsClient.send(
new FilterLogEventsCommand({ logGroupName: "/aws/lambda/orders-handler", filterPattern: requestId }),
);
}When the error itself is not enough, both SDKs can log the raw wire traffic.
# --- Python (boto3) ---
import logging
import boto3
boto3.set_stream_logger("botocore", level=logging.DEBUG)
# Now every request/response, including headers and retry attempts, is logged.
ddb = boto3.client("dynamodb")// --- TypeScript (AWS SDK v3) ---
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
const ddb = new DynamoDBClient({
logger: console, // logs each request/response lifecycle event to the console
});botocore debug logging is extremely verbose - enable it only while reproducing an issue, never in steady-state production.logger option accepts any object with debug/info/warn/error methods, so you can route it to your existing logger.The request id - ResponseMetadata.RequestId in boto3, $metadata.requestId in SDK v3. It is what ties your client-side error back to server-side context and what AWS Support asks for first.
No. Messages are human-readable and can change between SDK or service versions. Always branch on the error code (err.response["Error"]["Code"] in boto3, error.name in SDK v3).
Not necessarily - it means the SDK exhausted its retry budget for that call, or determined the error was not retryable at all. Debug logging shows you which happened.
Almost always, with one common exception: throttling errors are technically 4xx but are expected under load and are safely retryable.
No. It is very verbose and can log sensitive request/response bodies. Enable it temporarily to reproduce an issue, then turn it back off.
Sort it into a failure bucket - auth, throttling, retries, or consistency - and follow that bucket's defect walkthrough in this section for the specific diagnosis and fix.
Stack versions: This page was written for boto3 1.43.x (Python 3.10+) and the AWS SDK for JavaScript v3 (Node.js 18+).
Reviewed by Chris St. John·Last updated Jul 23, 2026