Runtime.UnhandledPromiseRejection: TypeError: Cannot read properties of undefined

Short answer

A promise rejected and no catch or await was there to receive it. Node treats that as a fatal condition and Lambda fails the invocation — the message you want is the inner error, not the wrapper, and the wrapper is what makes the real cause easy to miss.

What it means

A rejected promise carries an error to whoever is waiting for it. When nobody is waiting, Node has no one to hand it to, and since version 15 the default response is to treat that as fatal: the process raises unhandledRejection and terminates. Lambda's Node runtime catches this and reports the invocation as failed with Runtime.UnhandledPromiseRejection.

The wrapper is the problem when you are debugging. Runtime.UnhandledPromiseRejection describes the handling, not the fault — it says only that nothing caught the error. The error itself sits nested inside the envelope, under reason, with its own type, message and stack. That is the one worth reading, and it is easy to skim past because the outer message repeats a truncated version of it and the whole envelope arrives as one very long JSON line.

The genuinely hard version of this bug is the one where the rejection is not attributable to the invocation you are looking at. A promise created and abandoned during invocation A can reject milliseconds later, by which time the execution environment has been frozen and thawed for invocation B — and it is invocation B that fails. The stack trace points at code that had nothing to do with the request that died. This is precisely why grouping every line of an invocation together matters more here than almost anywhere else: the rejection's stack, the last thing invocation A logged, and the failure recorded against invocation B are three facts that only make sense next to each other.

The prevention is unglamorous and works: every promise is awaited or has a .catch(). The common leaks are the ones that feel harmless at the time — a metric publish nobody waits for, an async callback handed to forEach, a cache warm started at module scope. None of them look like error handling gaps, and all of them are.

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-29T10:14:07.114Z START RequestId: 8f2c4a71-3e09-4b62-a5d8-0c7e1b9f3a44 Version: $LATEST
2026-08-29T10:14:07.118Z 8f2c4a71-3e09-4b62-a5d8-0c7e1b9f3a44 INFO Order accepted order_id=ord_71c4
2026-08-29T10:14:07.166Z 8f2c4a71-3e09-4b62-a5d8-0c7e1b9f3a44 INFO Persisted to orders table
2026-08-29T10:14:07.244Z 8f2c4a71-3e09-4b62-a5d8-0c7e1b9f3a44 ERROR Unhandled Promise Rejection {"errorType":"Runtime.UnhandledPromiseRejection","errorMessage":"TypeError: Cannot read properties of undefined (reading 'endpoint')","reason":{"errorType":"TypeError","errorMessage":"Cannot read properties of undefined (reading 'endpoint')","stack":["TypeError: Cannot read properties of undefined (reading 'endpoint')"," at publishMetric (/var/task/src/metrics.js:18:31)"," at handler (/var/task/src/index.js:44:3)"]},"promise":{},"stack":["Runtime.UnhandledPromiseRejection: TypeError: Cannot read properties of undefined (reading 'endpoint')"]}
2026-08-29T10:14:07.288Z END RequestId: 8f2c4a71-3e09-4b62-a5d8-0c7e1b9f3a44
2026-08-29T10:14:07.288Z REPORT RequestId: 8f2c4a71-3e09-4b62-a5d8-0c7e1b9f3a44 Duration: 174.22 ms Billed Duration: 175 ms Memory Size: 512 MB Max Memory Used: 104 MB Status: error Error Type: Runtime.UnhandledPromiseRejection

LogStitch After

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

Error8f2c4a71-3e09-4b62-a5d8-0c7e1b9f3a44dur 174msbilled 175msmem 104/512MBlogs 6
  1. 10:14:07.114PLAT
    START RequestId: 8f2c4a71-3e09-4b62-a5d8-0c7e1b9f3a44 Version: $LATEST
  2. 10:14:07.118INFO
    INFO	Order accepted order_id=ord_71c4
  3. 10:14:07.166INFO
    INFO	Persisted to orders table
  4. 10:14:07.244ERROR
    ERROR	Unhandled Promise Rejection 	{"errorType":"Runtime.UnhandledPromiseRejection","errorMessage":"TypeError: Cannot read properties of undefined (reading 'endpoint')","reason":{"errorType":"TypeError","errorMessage":"Cannot read properties of undefined (reading 'endpoint')","stack":["TypeError: Cannot read properties of undefined (reading 'endpoint')","    at publishMetric (/var/task/src/metrics.js:18:31)","    at handler (/var/task/src/index.js:44:3)"]},"promise":{},"stack":["Runtime.UnhandledPromiseRejection: TypeError: Cannot read properties of undefined (reading 'endpoint')"]}
  5. 10:14:07.288PLAT
    END RequestId: 8f2c4a71-3e09-4b62-a5d8-0c7e1b9f3a44
  6. 10:14:07.288PLAT
    REPORT RequestId: 8f2c4a71-3e09-4b62-a5d8-0c7e1b9f3a44	Duration: 174.22 ms	Billed Duration: 175 ms	Memory Size: 512 MB	Max Memory Used: 104 MB	Status: error	Error Type: Runtime.UnhandledPromiseRejection
Error
Status
174ms
Duration
175ms
Billed
104/512MB
Memory
80%
Headroom
No
Cold start
Runtime.UnhandledPromiseRejection
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 runtime writes an Unhandled Promise Rejection envelope as a single JSON line containing errorType, errorMessage and a stack or reason array. Read the inner error inside it: the outer Runtime.UnhandledPromiseRejection tells you only that nothing caught it, while the inner one names what actually went wrong.

The most useful thing to check is where the stack points. A stack frame inside your handler is the easy case. A stack that contains only runtime frames, or the phrase "the promise rejected with the reason", means the rejection came from a promise created outside the handler's await chain — a fire-and-forget call — and the invocation that fails may not be the one that started it.

Causes, most likely first

1

An async call was made without await and without a catch

How to confirm

Look for calls that return promises and are neither awaited nor given a .catch() — a logging call, a metrics publish, a cache write, anything treated as fire-and-forget. These reject long after the line that started them, which is why the stack trace so often points somewhere that looks unrelated to the request.

2

A promise inside a callback that cannot receive it

How to confirm

Check for async functions passed to APIs that do not await their return value — forEach, event emitter listeners, setTimeout. The callback returns a promise into a caller that ignores it, so a rejection has nowhere to go.

3

A rejection in a Promise.all branch after another branch already rejected

How to confirm

Look for Promise.all in the code path. It settles on the first rejection while the other promises keep running; a second one rejecting afterwards has no remaining handler. Promise.allSettled shows this immediately by not rejecting at all.

4

A rejection during initialisation, outside any handler

How to confirm

Check whether the error appears near INIT_START rather than between START and END. Top-level async work — warming a connection pool, prefetching configuration — rejects with no invocation to attribute it to, and can fail a request that had not started when the promise was created.

Fixes

Fix 1

Await everything, or explicitly handle what you do not

The rule that removes this whole class of bug: every promise is either awaited or has a .catch(). If a call is genuinely fire-and-forget, say so in code, so the rejection is logged rather than fatal.

js// Rejects into nothing — fails a later invocation, with a confusing stack.
// publishMetric(name, value);

// Deliberately fire-and-forget, and says so.
void publishMetric(name, value).catch((error) => {
  console.warn("metric publish failed", { name, error: error.message });
});
Fix 2

Do not pass async functions to callbacks that ignore them

forEach discards the promise its callback returns, so a rejection inside it is unhandled by construction. Use a for…of loop when the work must be sequential, or Promise.all over a map when it can be parallel — both keep the promises in the await chain.

js// The promise returned by the callback is thrown away.
// records.forEach(async (record) => { await save(record); });

// Sequential, and every rejection propagates to the handler.
for (const record of records) {
  await save(record);
}

// Or parallel, still awaited.
await Promise.all(records.map((record) => save(record)));
Fix 3

Use allSettled when partial failure is acceptable

When several independent operations run together and one failing should not abandon the rest, Promise.allSettled waits for all of them and reports each outcome, so nothing rejects into a void.

jsconst results = await Promise.allSettled(
  targets.map((target) => notify(target))
);

const failed = results.filter((result) => result.status === "rejected");
if (failed.length > 0) {
  console.error("some notifications failed", {
    failed: failed.length,
    total: results.length
  });
}

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.

Runtime.UnhandledPromiseRejection: Error: connect ECONNREFUSEDUnhandled Promise RejectionERROR Unhandled Promise Rejection {"errorType":"Runtime.UnhandledPromiseRejection"}lambda unhandled promise rejection

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.