Failed to compile event @click:$(...) 里的 return、break 与 while
主线位置:交互主线的前半段。
$(...)由topcoat-runtime-grammar同时生成 Rust 和 JavaScript($(...) 表达式);事件处理器的闭包体在服务端不执行,只在浏览器里跑,所以 JS 一侧的问题编译期和服务端渲染都看不出来。
flowchart TB
src["$(|_e| { ... })"] --> grammar["runtime-grammar:逐个语法节点生成 JS"]
grammar --> ifn["if 包成立即调用的箭头函数"]
grammar --> tail["块的最后一个表达式前加 return"]
ifn --> r1["return 只退出 if:结果和 Rust 不同"]
ifn --> r2["break 越过函数边界:SyntaxError"]
tail --> r3["return while (...):SyntaxError"]
r2 --> fail["浏览器:Failed to compile event @click"]
r3 --> fail
现象
依赖:topcoat =0.10.0 的 full、tokio =1.54.1;页面挂了运行时(.assets(...)、.runtime()、script()),一个驱动脚本加载 500 毫秒后点按钮、把结果写进 <pre>,无头 Chrome --dump-dom 读出来。
let flag = signal(cx, || true);
let a = signal(cx, || 0.0f64);
// 1. if 里的 return
<button id="ret" @click=$(|_e| { if flag.get() { return a.set(1.0); } a.set(2.0) })>"ret"</button>
// 2. if 里的 break
<button id="brk" @click=$(|_e| { loop { if true { break; } break; } n.set(3.0) })>"brk"</button>
// 3. 块末尾的 while
<button id="brk-fixed" @click=$(|_e| { while m.get() < 3.0 { m.set(m.get() + 1.0); } })>"brk-fixed"</button>
三个都能编译,服务端照常渲染出页面。浏览器里:
ret: a=2 (按 Rust 语义应该是 1)
brk: n=0 (处理器没挂上,点击无反应)
"Uncaught Error: Failed to compile event @click", source: .../_topcoat/assets/topcoat-a85ca5b0b8314af5.js (3)
while: m=0 (同上)
"Uncaught Error: Failed to compile event @click", source: .../_topcoat/assets/topcoat-a85ca5b0b8314af5.js (3)
控制台只有「Failed to compile event @click」,真正的 SyntaxError 在错误的 cause 里,要展开才看得到;是哪个元素的处理器,消息里也没有。
原因
if 被包成函数。 expr_if 为了让 if 在表达式和语句位置都能用,把 JS 一侧写成 (() => { if (...) ... })()。函数边界挡住了 return(只退出这个箭头函数,后面的 a.set(2.0) 照样执行)、break 和 continue(JS 里越过函数的 break 是语法错误「Illegal break statement」)。这三条已经登记在 coherence 的 known-mismatches.toml(return_inside_if、break_inside_if、continue_inside_if)。
块末尾的 while 被加了 return。 这一条没有登记。stmt 把块里最后一个、没有分号的表达式当成块的值,在 JS 里前面写 return :
// A trailing expression (no semicolon) is the block's value, so
// it becomes the JavaScript `return`.
let returns = is_last && semi.is_none();
if returns {
js.push_str("return ");
}
Rust 里 while 是值为 () 的表达式,放在块末尾、不加分号完全合法;JS 里 while 是语句,生成出来的是:
(__local0) => { return while ((cx.hydrate({"t":"Signal","id":"a1bf..."})).get().lt(cx.hydrate(3.0)).dehydrate()) { ... }; }
return while 是语法错误。coherence 测试里有 while,但写的是 { while false {} 3.0 },while 不在末尾,所以没测到。tag 之后到 2026-10-14,expr/ 目录没有新提交。
改写
| 写法 | 改成 | 实测 |
|---|---|---|
{ if c { return x.set(1.0); } x.set(2.0) } | if c { x.set(1.0) } else { x.set(2.0) }:把提前返回改成 if / else 两个分支 | b=1,和 Rust 一致 |
loop { if c { break; } ... } | 把条件提到循环头:while !c { ... },并在 while 后面加分号 | — |
{ while c { ... } } | { while c { ... }; }:末尾加一个分号 | m=3,处理器正常 |
加了分号之后生成的 JS 是 { while (...) { ... }; },没有 return。Rust 一侧语义不变,块的值还是 ()。
经验规则:
$(...)的事件处理器里不要写提前return、不要在if里break/continue。- 块的最后一个表达式如果是
while、loop这类在 JS 里只能当语句的结构,后面加分号。(本页只实测了while。) - 逻辑复杂到需要这些控制流时,把它放进
#[procedure]在服务端算,或者用raw!直接写 JS。
怎么尽早发现
- 开发时开着浏览器控制台;看到「Failed to compile event」就展开
cause。 - 在测试里用无头浏览器打开每个页面,收集控制台的
Uncaught:服务端测试和cargo build都抓不到这一类错误。 - coherence 只比较「表达式的结果」,不覆盖你的页面;本页的
while就是它没覆盖到的写法。
小结
- 浏览器报「Failed to compile event @click」而服务端一切正常,多半是
$(...)生成的 JS 有语法错误;真正的SyntaxError在cause里。 if被包成立即调用的函数:return只退出if(实测得 2 不得 1),break/continue是语法错误;三条都在 coherence 的已知不一致里。- 块末尾不带分号的
while被生成为return while (...),同样编译失败;这一条没有登记,加一个分号就好。