Skip to content

Commit 41c43c0

Browse files
hasumikinclaude
andcommitted
Name the culprit when callbacks raise; alias Response#body to data
Two error-visibility gaps found while dogfooding: Event handlers re-raise into the JS bridge, where the console shows "Callback <id>: ArgumentError: ..." with no component or handler name. bind_events now prints "[Funicular] SomeComponent#handler (onclick) raised ..." first, so arity mistakes (def handler vs def handler(event)) are findable at a glance. HTTP response callbacks are worse: an exception out of the caller's block (settled == true) re-raises into the bridge and can vanish entirely. Six storefront components written against `response.body` froze on their loading state with no output at all. The re-raise now prints "[Funicular::HTTP] GET /api/... callback raised NoMethodError: ..." first -- and Response gains `body` as an alias of `data`, since that is what every mainstream HTTP client calls the payload. The mrblib changes reach browsers with the next picoruby.wasm rebuild; the SSR (CRuby) side and the new minitest coverage are live now. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 51ebffb commit 41c43c0

3 files changed

Lines changed: 77 additions & 2 deletions

File tree

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# frozen_string_literal: true
2+
3+
require "test_helper"
4+
5+
# The mrblib runtime is plain Ruby, so the CRuby suite can cover the
6+
# Response#body alias and the handler error report format directly;
7+
# the browser-side wiring ships with the next picoruby.wasm rebuild.
8+
class CallbackErrorVisibilityTest < Minitest::Test
9+
def setup
10+
Funicular::SSR::Runtime.load_framework!
11+
end
12+
13+
# Assigning the anonymous class to a constant names it, so the report
14+
# prints a real component name. Defined lazily: Funicular::Component
15+
# only exists after the framework loads.
16+
def probe_component
17+
unless defined?(::CheckoutProbeComponent)
18+
Object.const_set(:CheckoutProbeComponent, Class.new(Funicular::Component) do
19+
def render; end
20+
end)
21+
end
22+
::CheckoutProbeComponent.new
23+
end
24+
25+
def test_response_body_is_an_alias_of_data
26+
response = Funicular::HTTP::Response.new(200, { "id" => 1 })
27+
assert_equal response.data, response.body
28+
assert_equal({ "id" => 1 }, response.body)
29+
end
30+
31+
def test_report_handler_error_names_component_handler_and_event
32+
error = ArgumentError.new("wrong number of arguments (given 1, expected 0)")
33+
34+
out, _err = capture_io do
35+
probe_component.report_handler_error("click", "#handle_save", error)
36+
end
37+
38+
assert_includes out, "CheckoutProbeComponent#handle_save"
39+
assert_includes out, "(onclick)"
40+
assert_includes out, "ArgumentError: wrong number of arguments"
41+
end
42+
43+
def test_report_handler_error_never_raises
44+
capture_io do
45+
assert_nil probe_component.report_handler_error("click", "#x", RuntimeError.new("y"))
46+
end
47+
end
48+
end

mrblib/component.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,7 @@ def bind_events(dom_element, vnode)
656656
begin
657657
self.send(value, event)
658658
rescue => e
659+
report_handler_error(event_name, "##{value}", e)
659660
component_raised(e) if respond_to?(:component_raised)
660661
raise e
661662
end
@@ -672,6 +673,7 @@ def bind_events(dom_element, vnode)
672673
value.call(event)
673674
end
674675
rescue => e
676+
report_handler_error(event_name, "##{value.name}", e)
675677
component_raised(e) if respond_to?(:component_raised)
676678
raise e
677679
end
@@ -688,6 +690,7 @@ def bind_events(dom_element, vnode)
688690
value.call(event)
689691
end
690692
rescue => e
693+
report_handler_error(event_name, "(proc)", e)
691694
component_raised(e) if respond_to?(:component_raised)
692695
raise e
693696
end
@@ -720,6 +723,18 @@ def bind_events(dom_element, vnode)
720723
end
721724
end
722725

726+
# Name the culprit before an event handler error is re-raised into
727+
# the JS bridge, where "Callback <id>: ArgumentError: ..." carries no
728+
# hint of which component or handler it came from. Uses puts so the
729+
# message reaches the browser console (same idiom as the
730+
# ErrorBoundary logger). Never raises itself.
731+
def report_handler_error(event_name, handler, error)
732+
puts "[Funicular] #{self.class}#{handler} (on#{event_name}) raised " \
733+
"#{error.class}: #{error.message}"
734+
rescue
735+
nil
736+
end
737+
723738
# Collect ref elements from VDOM
724739
# Called by VDOM::Renderer and Patcher
725740
def collect_refs(dom_element, vnode, refs_map = {})

mrblib/http.rb

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ module HTTP
33
class Response
44
attr_reader :data, :status, :ok
55

6+
# Every mainstream HTTP client calls the payload `body`; keep
7+
# that name working alongside `data`.
8+
alias body data
9+
610
def initialize(status, data)
711
@status = status
812
@ok = @status >= 200 && @status < 300
@@ -125,8 +129,16 @@ def request(method, url, body, &block)
125129
# invalid URL) must still deliver a response -- a hanging
126130
# callback would hang the schema barrier and every REST
127131
# caller. An exception out of the caller's OWN block must
128-
# NOT settle a second time.
129-
raise e if settled
132+
# NOT settle a second time. It is re-raised into the JS
133+
# bridge, where it can vanish silently, so name the culprit
134+
# on the console first: a swallowed typo in a response
135+
# handler otherwise just freezes the page in its loading
136+
# state.
137+
if settled
138+
puts "[Funicular::HTTP] #{method} #{url} callback raised " \
139+
"#{e.class}: #{e.message}"
140+
raise e
141+
end
130142
settled = true
131143
if block
132144
block.call(Response.new(0,

0 commit comments

Comments
 (0)