Svelte → D IR{#await}, {#key}, boundary

{#await}, {#key}, <svelte:boundary>

These three constructs look like control flow and they are the ones that interact with time. Time is where wasm-eh try_table and Binaryen asyncify used to be mutually exclusive. The default engine cell is still wasm-eh. The etcimon/binaryen fork (binaryen/, branch svelte-d) can --asyncify that try_table module, so {#await} now prints .await — but a landing pad and libwasm_await__void still never share a try/catch. Catching a rejected promise happens after rewind.

{#await}

{#await job}
  <p>Wait</p>
{:then v}
  <p>Done {v}</p>
{:catch e}
  <p>Err</p>
{/await}

Pending, then, and catch are three @visible children of the owner. Then {v} is @prop named after the binding. The host field is a JsPromise. It starts pending. App.ready — after render, with live handles — calls wireAwait. Wiring in construct was too early, the same class of bug as wireEach.

wireAwait asks JS whether the ship module exported asyncify_get_state. If it did (fork wasm-opt --asyncify), it calls job.await. That import always resumes; a JS reject is recorded on the host and D reads libwasmAwaitFailed() after the import returns. Then it fills {:catch e} from libwasmAwaitError() and {:then v} from libwasmAwaitValue() (same assign as failBoundary) and flips the @visible flags. If the module is not asyncified (stock Binaryen 123/132), wireAwait falls back to JsPromise.then / .error; those callbacks note the Any handle (libwasmNoteAwaitOk / libwasmNoteAwaitFail) then read the same strings. Each {#await} job gets its own wireAwait block: the first keeps await_then / await_catch; later jobs use await_*_<job>. Two {e} / {v} bindings uniquify to eP / eP2. this.update.await_then still settles (Combo Go) when you want to flip without a real promise. No job at all: settle to at print time (G87). {#await job then v} (no pending block) is the same IR with no pending child.

Do not wrap .await in try/catch. Rewind re-enters the export from the top; landing-pad state is not part of Asyncify. {#await} catch is Svelte visibility, not a D exception crossing the import. A D throw/catch that should run after a failed await belongs in a different function, or after the await point without wrapping the import. throwBoundary stays same-function try/catch and still must not mention .await.

{#key}

{#key expr} prints a remount!"child"(this) helper. The child is torn down and compile!’d again. It is not a keyed each and it is not a new list. Fixture: ComboMore.svelte. Use it when the child’s identity should change with an expression; do not use it as a poor man’s {#each}.

<svelte:boundary>

A boundary is two visibilities, not a second exception runtime. The body is @visible with boundary_ok = true. {#snippet failed} is @visible with boundary_failed = false. this.update.boundary_failed hides the body and shows failed. this.update.boundary_ok resets. {#snippet failed(error, reset)} plus onerror emit failBoundary / resetBoundary; Retry is the snippet reset (G96).

throwBoundary is the construct that looks like it should be throw in one method and catch in another. It is not. The try, the throw, and the catch must be in the same function on the same struct as trip() (G105, ComboMedia / ComboSurf). That is the navbar EH path. The exception does not escape nothrow. After the catch, the method calls failBoundary. Personality is first-catch-type only. An exception that crosses a function boundary is an abort, not a failed snippet.

If you take one rule from this page, take that one. The rest of boundary is @visible. The throw path is same-function or it is not a boundary.