Describe the bug
In Solid 2.0.0-beta.20, yielding a Promise-like value whose then getter or then() method throws causes the returned action promise to reject, but the action iterator is never removed from its transition.
Any plain signal writes performed before the yield remain permanently held. Later writes to those signals rejoin the leaked transition and also never commit.
This violates the documented support for yielding "promises (or any awaitable)" and the existing action contract that partial plain writes commit when an action rejects.
Your Example Website or App
Focused reproduction against solidjs/solid next at ec432b40:
import { action, createSignal, flush } from "solid-js";
const [value, setValue] = createSignal(0);
const badThenable = Object.defineProperty({}, "then", {
get() {
throw new Error("then getter failed");
}
});
const run = action(function* () {
setValue(1);
yield badThenable as PromiseLike<void>;
});
await run().catch(error => {
console.log(error.message); // "then getter failed"
});
flush();
// Expected: 1. Plain writes before an action rejection commit.
// Actual: 0.
console.log(value());
setValue(2);
flush();
// Expected: 2.
// Actual: still 0; the leaked action keeps the transition incomplete.
console.log(value());
A thenable with a normal getter but a synchronously throwing method has the same failure:
const badThenable = {
then() {
throw new Error("then call failed");
}
};
Steps to Reproduce the Bug or Issue
- Start an action and write a plain signal.
- Yield an object whose
then getter or method throws synchronously.
- Catch the returned action promise rejection.
- Flush and read the signal.
- Write the signal again and flush.
- Observe that neither write ever commits.
Expected behavior
Thenable-assimilation failures should reject through the action's normal done(..., failed = true) path, remove the iterator from _actions, and let pre-rejection plain writes commit. Later signal writes must remain usable.
Screenshots or Videos
Not applicable.
Platform
- OS: macOS
- Runtime: Node.js 25
- Version: Solid 2.0.0-beta.20,
next at ec432b40
Additional context
The failure is in packages/solid-signals/src/core/action.ts:114-135.
step() catches exceptions from it.next() and it.throw(), but the yielded value is assimilated later in run():
if (isThenable(r.value))
return void r.value.then(
v => restoreTransition(ctx, () => step(v)),
e => restoreTransition(ctx, () => step(e, true))
);
Both isThenable(r.value) and r.value.then(...) can throw. Those exceptions escape the Promise executor, so JavaScript rejects the returned promise automatically, but Solid never calls done(). The iterator therefore remains in ctx._actions, making transitionComplete() false forever.
isThenable at packages/solid-signals/src/core/async.ts:129-135 directly reads the then property, which is where the getter variant throws.
Existing tests cover rejected promises, generator throws, and normally settling custom thenables, but not failures during thenable assimilation.
Describe the bug
In Solid 2.0.0-beta.20, yielding a Promise-like value whose
thengetter orthen()method throws causes the returned action promise to reject, but the action iterator is never removed from its transition.Any plain signal writes performed before the yield remain permanently held. Later writes to those signals rejoin the leaked transition and also never commit.
This violates the documented support for yielding "promises (or any awaitable)" and the existing action contract that partial plain writes commit when an action rejects.
Your Example Website or App
Focused reproduction against
solidjs/solidnextatec432b40:A thenable with a normal getter but a synchronously throwing method has the same failure:
Steps to Reproduce the Bug or Issue
thengetter or method throws synchronously.Expected behavior
Thenable-assimilation failures should reject through the action's normal
done(..., failed = true)path, remove the iterator from_actions, and let pre-rejection plain writes commit. Later signal writes must remain usable.Screenshots or Videos
Not applicable.
Platform
nextatec432b40Additional context
The failure is in
packages/solid-signals/src/core/action.ts:114-135.step()catches exceptions fromit.next()andit.throw(), but the yielded value is assimilated later inrun():Both
isThenable(r.value)andr.value.then(...)can throw. Those exceptions escape the Promise executor, so JavaScript rejects the returned promise automatically, but Solid never callsdone(). The iterator therefore remains inctx._actions, makingtransitionComplete()false forever.isThenableatpackages/solid-signals/src/core/async.ts:129-135directly reads thethenproperty, which is where the getter variant throws.Existing tests cover rejected promises, generator throws, and normally settling custom thenables, but not failures during thenable assimilation.