Skip to content

fix(handler): wait for context.succeed when handler returns undefined#785

Closed
zarirhamza wants to merge 1 commit into
mainfrom
zarir/sles-2891-undefined-return-regression
Closed

fix(handler): wait for context.succeed when handler returns undefined#785
zarirhamza wants to merge 1 commit into
mainfrom
zarir/sles-2891-undefined-return-regression

Conversation

@zarirhamza

@zarirhamza zarirhamza commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Restore the pre-v11.126.0 semantics for the (event, context) => { ... } handler shape (no callback parameter, no explicit return). When the handler returns undefined, the wrapper now waits for context.succeed / context.done / context.fail / callback instead of resolving the wrapping promise immediately with undefined.

This fixes a regression for handlers using aws-serverless-express@3's proxy(server, event, context) pattern (default CONTEXT_SUCCEED resolution mode) and any other "fire-and-forget then signal via context.*" handler shape.

Root cause

#661 (v11.126.0, "Fix Typescript Timeouts when Lambda handler returns undefined") added an eager-resolve branch:

} else if (asyncProm === undefined && handler.length < 3) {
  // Handler returned undefined and doesn't take a callback parameter, resolve immediately
  promise = Promise.resolve(undefined);
}

#665 (v12.127.0) refactored this and #683 (v12.130.0) added a looksLikeArtifact heuristic that keeps waiting when the handler returns a Server / EventEmitter. However, the artifact heuristic gates on asyncProm !== undefined, so when the handler returns nothing it still falls through to Promise.resolve(undefined) — i.e. #683 never covered the no-return case.

A handler that returns undefined synchronously is indistinguishable from a handler that's mid-flight and intends to complete via context.succeed / context.done / context.fail. Eager-resolving on undefined truncates that work: the Lambda runtime ships undefined back to the caller and freezes the worker before pending stdout writes flush to CloudWatch.

A representative handler shape that hits this:

exports.handler = (event, context) => {
  proxy(server, event, context);   // aws-serverless-express, fires context.succeed later
};

handler.length === 2, no return, so asyncProm === undefined → previously hit the eager-resolve branch and resolved before Express finished.

Fix

} else if (asyncProm === undefined) {
  // Handler returned nothing (implicit `undefined`) and doesn't take a callback parameter.
  // It must be relying on `context.succeed` / `context.done` / `context.fail` to signal
  // completion. Wait for callbackProm rather than resolving immediately.
  promise = callbackProm;
}

Additional small hardening: tighten looksLikeArtifact to require asyncProm !== null (the prior check accepted null and would then run instanceof EventEmitter / property reads on it).

Why this is safe to revert

  • This was the behavior shipped from the beginning through v11.125.0. Reverting restores a long-stable contract.
  • The Lambda Node.js runtime contract is "the function ends when the callback is called, context.succeed/done/fail is called, or the returned promise settles". It is not "the function ends when the synchronous body returns". (fix): Fix Typescript Timeouts when Lambda handler returns undefined #661's branch silently changed that contract.
  • A handler that returns undefined and does nothing else is now left waiting on callbackProm, which is correct: it will hit the configured Lambda function timeout. That is the expected behavior — a function that never signals completion is a user bug, and conflating it with "the handler is done" was the wrong fix.

Notes for users still hitting timeouts with TS-bundled async handlers

The original problem #661 set out to solve (TS compilation losing the implicit return of an async handler) is better addressed at the user level: ensure your transpilation target supports native async (Node ≥ 12), or return the work explicitly. The wrapper should not silently truncate execution to compensate.

Tests

PASS src/utils/handler.spec.ts
  promisifiedHandler
    ✓ returns a promise when callback used by the handler
    ✓ returns a promise when the original handler returns a promise
    ✓ throws an error when the original lambda gives the callback an error
    ✓ throws an error when the original lambda throws an error
    ✓ returns the first result to complete between the callback and the handler promise
    ✓ doesn't complete using non-promise return values
    ✓ completes when calling context.done
    ✓ completes when calling context.succeed
    ✓ throws error when calling context.fail
    ✓ waits for context.succeed when handler returns undefined and length < 3     ← new
    ✓ waits for context.done    when handler returns undefined and length < 3     ← new
    ✓ waits for context.fail    when handler returns undefined and length < 3     ← new
    ✓ simulates aws-serverless-express proxy() pattern                            ← new
    ✓ completes when handler returns a value directly (sync handler)
    ✓ waits for callback when handler returns non-promise artifact with callback parameter
    ✓ detects http.Server-like artifact (has listen AND close)
    ✓ detects EventEmitter-like artifact (has on AND emit)
    ✓ detects EventEmitter instance
    ✓ detects artifact by constructor name (Server/Socket/Emitter)
    ✓ does NOT treat plain response objects as artifacts
Tests: 20 passed, 20 total

The four added tests would have caught #661 and #665. The existing it("completes when handler returns undefined") (added by #661, codifying the regressive behavior) is replaced; the new tests assert the actual Lambda runtime contract.

Test plan

  • jest src/utils/handler.spec.ts — 20/20 pass
  • tslint --project tsconfig.json — clean
  • prettier --check src/utils/handler.{ts,spec.ts} — clean
  • tsc --noEmit — clean
  • Full suite (jest) — only 2 pre-existing failures in src/index.spec.ts (nock-based, unrelated to handler logic; reproduce on main without this PR)

Related

@zarirhamza zarirhamza requested review from a team as code owners June 11, 2026 14:43
@zarirhamza zarirhamza requested a review from shreyamalpani June 11, 2026 14:43
Restore the pre-v11.126.0 semantics for the `(event, context) => { ... }` /
`length < 3` / no-return handler shape: when `asyncProm === undefined`, fall
back to `callbackProm` instead of resolving immediately with `undefined`.

PR #661 (v11.126.0) introduced an "eager-resolve on undefined" branch intended
to fix TypeScript-transpiled async handlers whose `return` was lost during
bundling. PR #665 (v12.127.0) refactored that branch and PR #683 (v12.130.0)
added a `looksLikeArtifact` heuristic for handlers that *return* a Server /
EventEmitter, but the `asyncProm === undefined` path was never re-guarded. As
a result every layer from v11.126.0 onwards silently truncates handlers that
return nothing and finish via `context.succeed` / `context.done` /
`context.fail`, most notably `aws-serverless-express@3`'s
`proxy(server, event, context)` with the default `CONTEXT_SUCCEED` resolution
mode.

Symptom: handler returns `undefined` synchronously, wrapper resolves
immediately with `undefined`, the Lambda runtime ships `undefined` back to the
caller and freezes the worker before any deferred work writes to stdout, so
nothing is flushed to CloudWatch.

Tests:
- Replace the test added by PR #661 that codified the regressive "return
  undefined -> resolve immediately" behavior.
- Add four regression tests covering the actual contract: a handler with
  `length < 3` and no return must wait for `context.succeed` / `context.done` /
  `context.fail`, including a reproducer mirroring the aws-serverless-express
  `proxy()` shape.

Also tighten `looksLikeArtifact` to require `asyncProm !== null` (the previous
check accepted `null`, which would have thrown on the subsequent `instanceof`
and property-access reads).
@zarirhamza zarirhamza force-pushed the zarir/sles-2891-undefined-return-regression branch from a37ca3a to 80e5f09 Compare June 11, 2026 15:19
@zarirhamza zarirhamza changed the title fix(handler): wait for context.succeed when handler returns undefined (SLES-2891) fix(handler): wait for context.succeed when handler returns undefined Jun 11, 2026
@zarirhamza zarirhamza closed this Jun 11, 2026
@zarirhamza zarirhamza deleted the zarir/sles-2891-undefined-return-regression branch June 11, 2026 15:20
@zarirhamza

Copy link
Copy Markdown
Contributor Author

Superseded by #786 (rebranched off a non-customer-named branch). Closing this one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant