TypeError: Cannot read properties of undefined (reading 'customer')

Short answer

Your code reached into an object that turned out to be undefined. On Lambda this is overwhelmingly an event-shape problem: the event arrived structured differently from the one you tested against, and the property you expected two levels down was never there.

What it means

This is the most common runtime error in JavaScript generally, and on Lambda it has a characteristic cause that makes it more tractable than it looks. A handler is a function whose only input is a JSON document produced by something you did not write — API Gateway, EventBridge, SQS, S3, a direct invoke — and every one of those produces a different shape. The error means your code walked a path through that document and fell off it.

The property named in the message is the one being read, not the one that was missing. In event.detail.customer.id, an error mentioning customer means event.detail was present and customer was not. That is a small distinction that saves a lot of time: it tells you exactly how far down the chain the shape matched before diverging.

The event-source variations that produce this most often are worth knowing by name. API Gateway delivers event.body as a string, so reading properties from it directly gives undefined rather than an error, and the failure appears one level deeper than the actual mistake. REST APIs and HTTP APIs differ in structure, and HTTP APIs have two payload format versions that put the method and path in different places. EventBridge wraps your payload under detail. SQS delivers a Records array whose body is, again, a string. A fixture built from the wrong variant passes every local test and fails on the first real request.

What makes this error frustrating to debug from raw CloudWatch is not the stack trace, which is usually precise, but the missing half of the picture: the event that caused it. Without it you can see what the code expected and not what it received. With the invocation read as a whole — the input logged at the top, your own progress lines, the throw, and the REPORT that follows — the diff between a failing request and a working one is usually visible immediately.

Counts

CloudWatch’s Errors metric: The error escaped your handler, so Lambda reports the invocation as failed and CloudWatch’s Errors metric counts it. LogStitch classified the invocation above as uncaught.

What it looks like in CloudWatch

This is the shape the failure arrives in: the lines of one invocation scattered among everything else the log group received at the same moment.

CloudWatch Logs Before

6 raw lines, in the order CloudWatch delivered them.

2026-08-29T16:18:44.207Z START RequestId: 8ab2f944-c598-4499-a183-93a4b5c6d208 Version: $LATEST
2026-08-29T16:18:44.210Z 8ab2f944-c598-4499-a183-93a4b5c6d208 INFO Webhook received provider=stripe event=payment_intent.succeeded
2026-08-29T16:18:44.244Z 8ab2f944-c598-4499-a183-93a4b5c6d208 DEBUG Signature verified
2026-08-29T16:18:44.301Z 8ab2f944-c598-4499-a183-93a4b5c6d208 ERROR Invoke Error {"errorType":"TypeError","errorMessage":"Cannot read properties of undefined (reading 'customer')","stack":["TypeError: Cannot read properties of undefined (reading 'customer')"," at normalizeIntent (/var/task/src/stripe/normalize.js:41:28)"," at handler (/var/task/src/index.js:88:22)"," at process.processTicksAndRejections (node:internal/process/task_queues:95:5)"]}
2026-08-29T16:18:44.324Z END RequestId: 8ab2f944-c598-4499-a183-93a4b5c6d208
2026-08-29T16:18:44.324Z REPORT RequestId: 8ab2f944-c598-4499-a183-93a4b5c6d208 Duration: 117.82 ms Billed Duration: 118 ms Memory Size: 256 MB Max Memory Used: 88 MB Status: error Error Type: Runtime.Unknown

LogStitch After

The same lines, grouped into the invocation they belong to.

Error8ab2f944-c598-4499-a183-93a4b5c6d208dur 118msbilled 118msmem 88/256MBlogs 6
  1. 16:18:44.207PLAT
    START RequestId: 8ab2f944-c598-4499-a183-93a4b5c6d208 Version: $LATEST
  2. 16:18:44.210INFO
    INFO	Webhook received provider=stripe event=payment_intent.succeeded
  3. 16:18:44.244DEBUG
    DEBUG	Signature verified
  4. 16:18:44.301ERROR
    ERROR	Invoke Error 	{"errorType":"TypeError","errorMessage":"Cannot read properties of undefined (reading 'customer')","stack":["TypeError: Cannot read properties of undefined (reading 'customer')","    at normalizeIntent (/var/task/src/stripe/normalize.js:41:28)","    at handler (/var/task/src/index.js:88:22)","    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)"]}
  5. 16:18:44.324PLAT
    END RequestId: 8ab2f944-c598-4499-a183-93a4b5c6d208
  6. 16:18:44.324PLAT
    REPORT RequestId: 8ab2f944-c598-4499-a183-93a4b5c6d208	Duration: 117.82 ms	Billed Duration: 118 ms	Memory Size: 256 MB	Max Memory Used: 88 MB	Status: error	Error Type: Runtime.Unknown
Error
Status
118ms
Duration
118ms
Billed
88/256MB
Memory
66%
Headroom
No
Cold start
Runtime.TypeError
Error type

The panel on the right is generated by running the excerpt on the left through the same parser that powers the free web stitcher — it is what the tool actually produces for this input, not an illustration of it.

How to confirm it from the logs

The property name in parentheses tells you what was being read; the stack trace's first frame tells you where. Together they identify the exact expression, and the useful question is which part of the chain was undefined — in event.detail.customer.id, the error naming customer means event.detail existed and customer did not.

The decisive evidence is the event itself. If the function does not log its input, this error is nearly unfixable from logs alone: you can see what your code wanted and not what it got. Compare a failing invocation against a succeeding one from the same period — the difference in the events is the bug, and reading both invocations end to end is faster than reasoning about the stack.

Causes, most likely first

1

The event source produces a different shape than the one you developed against

How to confirm

Compare the failing event against your test fixture. API Gateway REST and HTTP APIs differ, payload format 1.0 and 2.0 differ, a direct invoke differs from an event-source mapping, and an EventBridge event nests the payload under detail. A fixture copied from documentation for the wrong variant produces exactly this on the first real request.

2

The body is a JSON string that was never parsed

How to confirm

Check whether the code reads properties directly from event.body. API Gateway delivers the body as a string, so event.body.customerId is undefined rather than an error, and the failure surfaces one property deeper where it is much less obvious.

3

The field is genuinely optional and this request omitted it

How to confirm

Look at how many invocations fail versus succeed. A small, steady fraction failing on the same property means the field is absent for a subset of real traffic — an anonymous user, a legacy client, a record written before a schema change — rather than the shape being wrong for everyone.

4

An upstream call returned nothing and its result was used anyway

How to confirm

Check whether the undefined value came from the event at all, or from a previous call. A DynamoDB GetItem for a key that does not exist returns a response with no Item, and result.Item.name fails the same way as a bad event would.

5

The body is base64-encoded

How to confirm

Check event.isBase64Encoded. When it is true, event.body is base64 and parsing it as JSON yields nothing useful, so every property read from it is undefined.

Fixes

Fix 1

Log the event before you touch it

One line at the top of the handler turns this from guesswork into a two-second diagnosis, because the log then contains both what arrived and what your code did with it. Redact anything sensitive rather than skipping the log.

jsexport const handler = async (event, context) => {
  console.log("event", JSON.stringify({
    source: event.source ?? event.requestContext?.http?.method,
    keys: Object.keys(event),
    isBase64Encoded: event.isBase64Encoded
  }));
  // ...
};
Fix 2

Parse and validate the input at the boundary

Validate the event once, at the top, and fail with a message that names what was wrong. A schema validator turns a TypeError three frames deep into a 400 that says which field is missing.

jsimport { z } from "zod";

const Order = z.object({
  customerId: z.string(),
  items: z.array(z.object({ sku: z.string(), qty: z.number().int() }))
});

export const handler = async (event) => {
  const raw = event.isBase64Encoded
    ? Buffer.from(event.body, "base64").toString()
    : event.body;

  const parsed = Order.safeParse(JSON.parse(raw ?? "{}"));
  if (!parsed.success) {
    return { statusCode: 400, body: JSON.stringify({ errors: parsed.error.issues }) };
  }
  // parsed.data is now known-good
};
Fix 3

Use optional chaining where absence is legitimate

Where a field really is optional, say so in the code. Optional chaining and a default make the intent explicit and stop an absent value becoming an exception three lines later.

js// Throws when detail is absent.
// const customer = event.detail.customer.id;

// Explicitly tolerates absence, and fails with a useful message if it matters.
const customer = event.detail?.customer?.id;
if (!customer) {
  throw new Error(`No customer id in event (keys: ${Object.keys(event).join(", ")})`);
}

Also seen as

The same underlying failure, worded differently by a different runtime, SDK version, or logging layer. All of these land here — there is no separate page for each phrasing.

TypeError: Cannot read property 'body' of undefinedTypeError: Cannot read properties of null (reading 'Records')TypeError: Cannot destructure property 'id' of 'event.body' as it is undefinedlambda cannot read properties of undefined

Errors that show up alongside this one, or that people mistake for it.

References

LogStitch finds this automatically, across every invocation in your account.

Paste a log excerpt into the free web stitcher and see it grouped, classified, and measured in your browser — nothing is uploaded. Or run the Mac app against your own AWS profiles and get the same view over every function you own.