Skip to content

Fix uncalled rx.toast hint and dropped-result warning lookup - #7796

Merged
FarhanAliRaza merged 7 commits into
mainfrom
fix/mistaken-handler-return-values
Nov 11, 2026
Merged

FarhanAliRaza merged 7 commits into
mainfrom
fix/mistaken-handler-return-values

Conversation

@masenf

@masenf masenf commented Nov 11, 2026 •

Copy link
Copy Markdown
Collaborator

Status

The original change in this PR landed on main through the squash merge of the stacked #7821 (554eb10). The branch now merges main, so the diff holds only two follow-up fixes to that code:

  • Uncalled rx.toast hint. For an uncalled rx.toast or rx.toast.<type>, the "Did you mean to call it" hint named the internal Toaster.send_toast(...) / Toaster.toast_*(...) methods, and the tests asserted that text. _creator_reference now names a creator rx.<name> when reflex exports it under that name, such as rx.redirect(...). For any other creator defined by reflex, whose name is internal, the hint is "Did you mean to call it?" with no name. An app's own creator keeps its qualified name, such as _go_home(...).
  • Dropped-result warning lookup. _warn_dropped_result indexed app._registration_context.event_handlers[name], which raises KeyError in the done callback when the name is not in the app's registry. It now uses .get(name) and falls back to the event name. In a running app, the processor resolves the handler from the same registry, so this is a robustness fix only.

Test plan

  • test_returned_mistaken_event_raises_type_error: 3 toast cases now expect "Did you mean to call it?" / "Did you mean to call them?"; a new case returns an app's own uncalled creator and expects "Did you mean to call it: _go_home(...)?".
  • New test_dropped_result_warning_names_an_unregistered_event in tests/units/test_app.py. It and test_on_event_warns_once_in_dev_when_a_frontend_result_is_dropped assert the full warning message.
  • All 4 fail against main's source and pass with the fix. The 30 related targeted tests pass; ruff, pyright and pre-commit are clean.

News fragments: news/+dropped-result-warning-lookup.bugfix.md, packages/reflex-base/news/+uncalled-toast-hint.bugfix.md.

Original description (landed via #7821)

Fixes FINDING-015 from the reflex 0.11.0a1 pre-release testing (artifacts on branch claude/vibrant-pasteur-3921g3, prerelease-testing/runs/2026-11-11-v0.11.0a1/events_await/ and verify_evt/).

Defect

Since handlers may return a value to the backend code awaiting them (#7673), every non-event return value became the handler's result. That silently swallowed the mistakes 0.10.1 reported with TypeError: Your handler ... must only return/yield: None, Events or other EventHandlers referenced by their class ...: returning self.other_handler (meant type(self).other_handler), an uncalled event creator such as rx.redirect, or a list holding one. On 0.11.0a1 the chained handler never runs and nothing is logged or toasted.

Fix

The rule stays narrow on purpose, so handlers keep returning arbitrary values to their awaiting callers: only values that can never be a meaningful result raise.

  • base_state_processor.py: after _is_event_like rejects a value, _is_invalid_result flags (as before) an unawaited coroutine, plus
    • a handler function bound to its state or to a background task's state proxy (self.other_handler), recognised as a bound method whose __self__.event_handlers[name].fn is the function; and the stand-in a background handler binds to, now marked _reflex_bound_background_task;
    • a callable annotated to return an EventSpec (every rx.* event creator incl. rx.toast; reflex does not otherwise mark creators — an unannotated helper is treated as a result);
    • a list/tuple holding either.
      These go through the existing invalid-yield path: the delta is flushed, then the 0.10.1 TypeError naming the handler is raised (backend exception handler / error toast), for clicked, awaited and background handlers alike. The check only runs after _is_event_like failed and starts with callable(), so handlers returning None or events never reach it.
  • reflex/app.py EventNamespace.on_event: in dev mode only, when a handler triggered from the frontend returns a non-None value (other than []), warn once per handler that the value is dropped and only awaiting backend code receives it. on_event is the one place that owns the discarded future, so the check is exact; the dev flag is read once, so prod pays one attribute check per event.
  • Docs: one rules bullet and one alert sentence in docs/events/chaining_events.md.

Interaction with #7761

This PR is expected to merge before #7761 ("Stream event handler yield and return values to the frontend caller"). When rebasing #7761 onto it:

  • _is_result can become not _is_invalid_result(value) after its None/event checks (replacing callable(value) or iscoroutine(value) or _holds_events_or_coroutines(value), which would reject legitimate callable results).
  • The on_event dev warning must be skipped for events that carry a request id (their value is streamed to the frontend), and the docs/news wording "is dropped" should be narrowed accordingly.

Test plan

  • tests/units/reflex_base/event/processor/test_base_state_processor.py:
    • test_returned_mistaken_event_raises_type_error (14 cases: sync / async / sync-generator / background handlers returning self.target; self.bg_target; uncalled rx.redirect, rx.toast, rx.toast.success; [self.target]; (rx.call_script,); sync and async yields of self.target and rx.redirect; awaiting inline a handler that returns self.target) — each also asserts the target never ran.
    • test_returned_callable_that_is_not_an_event_resolves_future (4 cases: private helper, len, functools.partial, a Mock).
    • test_returned_event_reference_still_chains (3 cases).
  • tests/units/test_app.py: test_on_event_warns_once_in_dev_when_a_frontend_result_is_dropped (6 cases), test_on_event_does_not_watch_frontend_results_in_prod; test_on_event_uses_frontend_traceparent's fake enqueue now returns a future.
  • Failing before on unfixed main: 11 of the 14 mistaken-value cases (Failed: DID NOT RAISE TypeError for returns_bound_handler, async_returns_bound_handler, generator_returns_bound_handler, returns_bound_background_handler, background_returns_bound_handler, returns_uncalled_redirect, returns_uncalled_toast, returns_uncalled_toast_success, returns_list_holding_bound_handler, returns_tuple_holding_uncalled_call_script, awaits_bound_handler_return) and all 6 warning cases; the 3 yield cases and the result/chaining cases pass before and after as guards. test_handler_return_value_resolves_future is untouched and passes.
  • After: the two touched modules 633 passed; full unit suite 15663 passed, 66 skipped (the Playwright browser-driver module could not run locally for lack of a Chromium binary). ruff check, ruff format, pyright reflex tests, pre-commit on changed files: clean.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • This change requires a documentation update (included)

Known limits

  • Event-creator detection relies on the -> EventSpec return annotation; an unannotated creator or one returning EventSpec | None is treated as a result (and gets the dev warning when clicked).
  • An inherited handler accessed through a background StateProxy comes back as a functools.partial and is not recognised.
  • The dev warning covers events from the frontend only, not chained or background-enqueued ones.
  • The error keeps the exact 0.10.1 text (reads "Returned events of types <class 'function'>" for an uncalled rx.redirect).

News fragments: news/+mistaken-handler-return-values.bugfix.md and packages/reflex-base/news/+mistaken-handler-return-values.bugfix.md.


Generated by Claude Code

View guided diff Turn on auto-fix

…result is dropped

Since handlers may return a value to the backend code awaiting them (#7673),
any non-event return value became the handler's result. That silently swallowed
the classic mistakes 0.10.1 reported with "must only return/yield": returning
`self.other_handler` (meant `type(self).other_handler`), an uncalled event
creator such as `rx.redirect`, or a list holding one. The chained handler never
ran and nothing was logged.

Those values can never be a meaningful result, so they raise the 0.10.1
TypeError again, naming the user's handler:
- a handler function bound to its state (or a background task's state proxy),
  and the stand-in a background handler binds to, now marked so it can be
  recognized;
- a callable annotated to return an EventSpec (every rx.* event creator,
  rx.toast included), since reflex does not otherwise mark event creators;
- a sequence holding either.

Any other value stays the result. In dev mode, on_event warns once per handler
when a handler the frontend triggered returns a value, which nothing receives;
the frontend discards that future, so the check is exact and costs nothing in
prod.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dxjadq3qkhKnqZR2b9WBFi
@masenf
masenf requested review from a team and Alek99 as code owners November 11, 2026 07:36
@masenf masenf added this to the v0.11.0 milestone Nov 11, 2026
@masenf masenf added the on deck PRs lined up to review / merge next label Nov 11, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 8 files

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread tests/units/test_app.py Resolved
Comment thread docs/events/chaining_events.md Resolved
@greptile-apps

greptile-apps Bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Low impact] The PR appears safe to merge; no blocking issues remain.

Summary

The PR fixes two diagnostic messages:

  • Toast hints no longer show internal Reflex method names.
  • Dropped-result warnings use the event name when its handler is unregistered.

The latest changes strengthen tests by checking the full warning text. No new issues were found.

Previous threads were also checked: logger.warning replaces console.warn, and _creator_reference no longer scans module globals. masenf declined broader detection of renamed handlers because decorators without functools.wraps already produce an unsupported, warned-about setup; Greptile withdrew that finding.

All supplied previous threads were unnumbered, so none require a previousFindings entry.

Reviews (6) · Last reviewed commit: "Assert the full dropped-result warning i..." · Reviewed by Greptile

Comment thread reflex/app.py Resolved
@codspeed

codspeed Bot commented Nov 11, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

1 warning

⚠️ 12 benchmarks are dominated by system calls

System calls cannot be simulated consistently and are left out of the measure. The reported value understates the real cost. Switch to the Walltime instrument to measure them.

Learn more about system calls

See affected benchmarks

✅ 151 untouched benchmarks


Comparing fix/mistaken-handler-return-values (e8d5932) with main (3184cad)

Open in CodSpeed

When a handler returns or yields an event creator without calling it, the
0.10.1 "must only return/yield" message now ends with
"Did you mean to call it: rx.redirect(...)?", naming the creator as `rx.<name>`
when reflex exports it under that name, else by its qualified name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dxjadq3qkhKnqZR2b9WBFi
- on_event benchmark: the mocked enqueue returned a MagicMock, so the dev-mode
  done callback on the event's future ran as an AsyncMock call per event
  (call recording plus a never-awaited coroutine), doubling the measured time.
  enqueue now returns a resolved future, as the real EventFuture is; the
  callback's real cost is a few microseconds per event, in dev only.
- The dropped-result warning goes through the module logger with a
  once-per-handler set; console.warn is deprecated and added its own notice.
- The "call it" hint names a creator from its own function, without scanning
  module globals; rx.toast now reads Toaster.send_toast(...).
- The dropped-result test runs in a clean registration context.
- News fragment and docs limit the dropped-value wording to non-event values.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dxjadq3qkhKnqZR2b9WBFi
@masenf

masenf commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Re the CodSpeed report (#7796 (comment)), test_on_event_router_data 1.8 ms → 3.3 ms:

Cause. The slowdown came from the benchmark's mock, not from production code. In dev mode, on_event now adds a done callback to the future that enqueue returns. The benchmark mocks enqueue with AsyncMock(), whose return value is a mock. So each future.add_done_callback(...) ran as a second mock call per event: call recording plus a coroutine that was never awaited. cProfile showed 4000 mock calls for 2000 events where main has 2000, and that roughly doubles the measured time. The return/yield validation is not involved, because it only runs once a handler has returned a non-event value.

Fix (9679a8f). The benchmark's enqueue now returns a resolved future, as the real EventFuture is. Local medians, µs per 10 events:

harness dev (callback on) prod (no callback, same on_event as main)
old (mock future) 502 214
new (real future) 241 206

With a real future, the remaining cost is a few µs per event, in dev only. Prod adds a single attribute check. Because this PR changes the benchmark, CodSpeed will compare the new harness against main's old one, so the next report may show this benchmark as faster than main.


Generated by Claude Code

masenf pushed a commit that referenced this pull request Nov 11, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dxjadq3qkhKnqZR2b9WBFi
@masenf

masenf commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Correction to my comment above. The report for 9679a8f does not show test_on_event_router_data faster than main: it shows 1.8 ms → 2.0 ms (−9.8%). That is the dev-mode cost I measured locally with the real-future harness (241 vs 206 µs per 10 events, roughly 3–4 µs per event). The benchmark runs on_event in dev mode, where it now adds a done callback to each event's future so it can warn once about a dropped return value. Prod skips the callback and adds only an attribute check. So the remaining regression is real but limited to dev mode. Whether a once-per-handler dev warning is worth that per-event cost is the reviewer's call; the alternative is to drop the warning and keep only the error for mistaken returns.


Generated by Claude Code

@masenf

masenf commented Nov 11, 2026 •

Copy link
Copy Markdown
Collaborator Author

This PR's changes are already on main: #7821 was stacked on it, so the squash merge of #7821 (554eb10) also carries this PR's three commits. git diff 554eb10a6 9679a8f93 touches only the 4 files that #7821 itself changed (its empty-token check, test and fragment, shown reversed). Nothing is left to merge here, so this PR can be closed as landed via #7821. The open question about the dev-only done-callback cost (comment above) now applies to main. If you'd rather drop the dev warning, I'll open a follow-up PR.


Generated by Claude Code

masenf pushed a commit that referenced this pull request Nov 11, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dxjadq3qkhKnqZR2b9WBFi
FarhanAliRaza
FarhanAliRaza previously approved these changes Nov 11, 2026
masenf pushed a commit that referenced this pull request Nov 11, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dxjadq3qkhKnqZR2b9WBFi
The "Did you mean to call it" hint named the internal Toaster.send_toast
and Toaster.toast_* methods for an uncalled rx.toast or rx.toast.<type>.
Name a creator only when reflex exports it as rx.<name>; otherwise ask
"Did you mean to call it?" without a name.

The dev-mode dropped-result warning indexed the app's handler registry
directly. Use .get() and fall back to the event name.
@FarhanAliRaza FarhanAliRaza changed the title Raise again for mistaken handler return values; warn when a frontend result is dropped Fix uncalled rx.toast hint and dropped-result warning lookup Nov 11, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 6 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

@masenf

masenf commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Update to my earlier comment that this PR could be closed because #7821's squash carried it. FarhanAliRaza's 2540402, on top of a merge of main, adds two fixes that are not on main, so this PR should now merge rather than close:

  • The "Did you mean to call it" hint no longer names the internal Toaster.send_toast / Toaster.toast_* for an uncalled rx.toast or rx.toast.<type>. When the creator is not exported as rx.<name>, it asks without a name.
  • The dev-mode dropped-result warning looks the handler up with .get() and falls back to the event name instead of raising KeyError.

Merged into main, the diff is those 6 files (+35/−11). Locally, test_base_state_processor.py and test_app.py pass (648), and ruff and pyright are clean on the two source files.


Generated by Claude Code

masenf pushed a commit that referenced this pull request Nov 11, 2026
2540402 dropped the name for every creator reflex does not export as
rx.<name>, which also hid an app's own helper annotated to return an
EventSpec. Only a creator defined by reflex has an internal name; an
app's creator is named by its qualified name again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dxjadq3qkhKnqZR2b9WBFi
masenf pushed a commit that referenced this pull request Nov 11, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dxjadq3qkhKnqZR2b9WBFi

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 2 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

@masenf

masenf commented Nov 11, 2026 •

Copy link
Copy Markdown
Collaborator Author

CodSpeed on 2540402 flags test_state_update_wire_serialization[model_1000] (−4.85%) and test_state_collection_assignment[100000] (−4.6%). Neither comes from this PR. Its source changes are an error-message helper that runs only when a handler returns an invalid value, and a dev-mode warning lookup, so neither benchmark reaches them. #7809, a migrations-only PR, shows the same model_1000 drop (−5.4%) against the same main base. CodSpeed also reports three benchmarks in this run measured on different hardware.

Update for 439858e: only test_process_event_burst_independent_tokens[1] (−4.5%) is flagged. It is the same drift as on #7809 (−4.2%) and #7815 (−4.2%), whose changes are confined to migrations. An event burst whose handlers return None never builds the invalid-return hint.


Generated by Claude Code

Compare the whole warning message instead of substrings, through one
helper that builds the expected text.

@masenf masenf left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

👍

@FarhanAliRaza
FarhanAliRaza merged commit 51d8f50 into main Nov 11, 2026
168 checks passed
@FarhanAliRaza
FarhanAliRaza deleted the fix/mistaken-handler-return-values branch November 11, 2026 19:03
masenf added a commit that referenced this pull request Nov 11, 2026
* Raise again for mistaken handler return values; warn when a frontend result is dropped

Since handlers may return a value to the backend code awaiting them (#7673),
any non-event return value became the handler's result. That silently swallowed
the classic mistakes 0.10.1 reported with "must only return/yield": returning
`self.other_handler` (meant `type(self).other_handler`), an uncalled event
creator such as `rx.redirect`, or a list holding one. The chained handler never
ran and nothing was logged.

Those values can never be a meaningful result, so they raise the 0.10.1
TypeError again, naming the user's handler:
- a handler function bound to its state (or a background task's state proxy),
  and the stand-in a background handler binds to, now marked so it can be
  recognized;
- a callable annotated to return an EventSpec (every rx.* event creator,
  rx.toast included), since reflex does not otherwise mark event creators;
- a sequence holding either.

Any other value stays the result. In dev mode, on_event warns once per handler
when a handler the frontend triggered returns a value, which nothing receives;
the frontend discards that future, so the check is exact and costs nothing in
prod.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dxjadq3qkhKnqZR2b9WBFi

* Hint to call an uncalled event creator in the invalid return/yield error

When a handler returns or yields an event creator without calling it, the
0.10.1 "must only return/yield" message now ends with
"Did you mean to call it: rx.redirect(...)?", naming the creator as `rx.<name>`
when reflex exports it under that name, else by its qualified name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dxjadq3qkhKnqZR2b9WBFi

* Address review feedback; give the on_event benchmark a real future

- on_event benchmark: the mocked enqueue returned a MagicMock, so the dev-mode
  done callback on the event's future ran as an AsyncMock call per event
  (call recording plus a never-awaited coroutine), doubling the measured time.
  enqueue now returns a resolved future, as the real EventFuture is; the
  callback's real cost is a few microseconds per event, in dev only.
- The dropped-result warning goes through the module logger with a
  once-per-handler set; console.warn is deprecated and added its own notice.
- The "call it" hint names a creator from its own function, without scanning
  module globals; rx.toast now reads Toaster.send_toast(...).
- The dropped-result test runs in a clean registration context.
- News fragment and docs limit the dropped-value wording to non-event values.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dxjadq3qkhKnqZR2b9WBFi

* Fix uncalled rx.toast hint and dropped-result warning lookup

The "Did you mean to call it" hint named the internal Toaster.send_toast
and Toaster.toast_* methods for an uncalled rx.toast or rx.toast.<type>.
Name a creator only when reflex exports it as rx.<name>; otherwise ask
"Did you mean to call it?" without a name.

The dev-mode dropped-result warning indexed the app's handler registry
directly. Use .get() and fall back to the event name.

* Keep the name of an app's own event creator in the call hint

2540402 dropped the name for every creator reflex does not export as
rx.<name>, which also hid an app's own helper annotated to return an
EventSpec. Only a creator defined by reflex has an internal name; an
app's creator is named by its qualified name again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dxjadq3qkhKnqZR2b9WBFi

* Assert the full dropped-result warning in tests

Compare the whole warning message instead of substrings, through one
helper that builds the expected text.

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Farhan <www.mfarvirus@gmail.com>
(cherry picked from commit 51d8f50)
masenf pushed a commit that referenced this pull request Nov 11, 2026
…last open

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dxjadq3qkhKnqZR2b9WBFi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

on deck PRs lined up to review / merge next

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants