Skip to content

fix(client): call a legacy callback once when it throws - #3546

Merged
nkaradzhov merged 1 commit into
redis:masterfrom
maxymlyskov:fix/legacy-callback-once-when-it-throws
Oct 28, 2026
Merged

nkaradzhov merged 1 commit into
redis:masterfrom
maxymlyskov:fix/legacy-callback-once-when-it-throws

Conversation

@maxymlyskov

@maxymlyskov maxymlyskov commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Description

Legacy-mode callbacks are wired as promise.then(reply => callback(null, reply)).catch(err => callback(err)), so when the callback throws on success, the .catch calls it again with its own error. That's in all four places that call back: the generated commands (#createCommand), sendCommand, quit and multi().exec.

With a callback that throws, against Redis 8.10.2, on master:

set: callback ran 2 times: reply="OK" | err=bug in set callback
sendCommand: callback ran 2 times: reply="v" | err=bug in sendCommand callback
exec: callback ran 2 times: reply=["v"] | err=bug in exec callback

With this change each one runs once.

The reply transform (TRANSFORM_LEGACY_REPLY, and transformReplies for exec) now runs in its own .then, and the callback is called from .then(reply => callback(null, reply), err => callback(err)), so a transform error like the MultiErrorReply from exec still reaches the callback. A throw from the callback now ends as an unhandled rejection, same as master after the second call.

Added a test per call site in legacy-mode.spec.ts; all 4 fail on master. npm run build && npm test passes and lint is clean, apart from the sentinel destroy() from an end listener tears down a reentrant reopen case, which failed once in the full run here and passes 5 out of 5 when run alone.


Checklist

  • Does npm test pass with this change (including linting)?
  • Is the new or changed code fully tested?
  • Is a documentation update included (if this change modifies existing APIs, or introduces new ones)?

Note

Medium Risk
Touches promise/callback wiring on all legacy API entry points; behavior change for throwing callbacks (no second err callback) but aligns with expected Node semantics and is well covered by new tests.

Overview
Fixes double invocation of legacy-mode Node callbacks when the user callback throws after a successful Redis reply.

Previously, success handlers ran inside .then(...) with a following .catch(err => callback(err)), so a throw from callback(null, reply) was treated as a rejection and triggered a second call with that error. sendCommand, generated commands (e.g. set), quit, and multi().exec all used this pattern.

The implementation now invokes the callback from a single .then(onFulfilled, onRejected) so throws from the callback propagate as unhandled rejections instead of a second callback. Where TRANSFORM_LEGACY_REPLY or transformReplies applies, the transform runs in a separate .then first so transform/exec errors still reach the callback once.

Regression coverage adds a shared callsOfThrowingCallback helper and one test per call site, asserting exactly one [null, reply] and that the thrown error surfaces as unhandled.

Reviewed by Cursor Bugbot for commit c1c7b2c. Bugbot is set up for automated code reviews on this repo. Configure here.

Every legacy callback ran inside a .then whose rejection the following
.catch handled, so an error thrown by the callback on success was passed
back to the same callback as its error. Run the reply transform in its
own .then and call back from a two-argument .then, so a transform error
still reaches the callback and an error thrown by the callback is not
fed back into it.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ⚠️ Failed 2026-10-27T15:36:50.568954Z
We couldn’t complete this review. Comment @codex review to try again.
c1c7b2c PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@nkaradzhov nkaradzhov left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, LGTM.

@nkaradzhov
nkaradzhov merged commit a621a01 into redis:master Oct 28, 2026
15 checks passed
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.

2 participants