Available since v0.1.0
fs-assert-mutex
StableControls mutual exclusion of conditional assertions across different types on the same element. Without mutex, same-type conditionals (added-success / added-error) race naturally while cross-type conditionals (removed-success / added-error) resolve independently. fs-assert-mutex lets you override that default.
Syntax
fs-assert-mutex="<mode>"
Four modes:
| Mode | Semantics |
|---|---|
type |
Same-type conditionals race, cross-type are independent (this is the default when fs-assert-mutex is omitted) |
each |
All conditionals on the element race as one group — first to resolve wins, all others dismissed |
conditions |
Condition keys form outcome groups — when one key wins, assertions with different keys are dismissed but same-key assertions resolve independently |
<key1>,<key2> |
Selective — only listed condition keys compete; unlisted keys resolve independently |
fs-assert-mutex="" is invalid and logs a warning. Always provide an explicit mode.
When to use it
each— "exactly one of these outcomes should win, then stop" — the canonical choice for delete flows (removed OR error) and login flows (dashboard OR error).conditions— "success needs several things, but if error wins it cancels them all" — useful when one condition key implies multiple assertion types.- selective (
success,error) — "these two race, but let this third assertion resolve independently" — useful for analytics pixels that should always fire regardless of success/error.
mutex="each" — first wins, all others dismissed
All conditionals race. First to resolve wins. All others are dismissed.
<button fs-assert="todos/remove-item" fs-trigger="click"
fs-assert-mutex="each"
fs-assert-removed-success=".todo-item"
fs-assert-added-error=".error-msg"
fs-assert-timeout="5000">
Delete
</button>
If .todo-item is removed, removed-success wins and added-error is dismissed. If the server returns an error and .error-msg appears first, added-error wins and removed-success is dismissed.
mutex="conditions" — condition keys are outcome groups
Condition keys form groups. When one key wins, assertions with different keys are dismissed. Same-key assertions resolve independently.
<button fs-assert="todos/add-item" fs-trigger="click"
fs-assert-mutex="conditions"
fs-assert-added-success=".todo-item"
fs-assert-emitted-success="todo:added"
fs-assert-added-error=".add-error">
Add
</button>
Two outcome groups: success and error.
- When
successwins: bothadded-successandemitted-successresolve independently (first-to-resolve starts the group, siblings in the same group still need to pass).added-erroris dismissed. - When
errorwins: both success assertions are dismissed.
This is the right choice when "success" means "N things must happen" but "error" means "a totally different path was taken."
mutex="<key1>,<key2>" — selective competition
Only listed condition keys race. Unlisted keys resolve independently.
<button fs-assert="checkout/submit" fs-trigger="click"
fs-assert-mutex="success,error"
fs-assert-added-success=".confirmation"
fs-assert-added-error=".error-msg"
fs-assert-updated-analytics="#tracking-pixel">
Submit
</button>
success and error are mutually exclusive. analytics resolves independently — the tracking pixel should always update regardless of checkout outcome.
Default (mutex omitted or mutex="type")
Same-type conditionals race (added-success vs added-error), cross-type conditionals are independent (removed-success + added-error each produce their own signal). This is fine when you genuinely want both cross-type outcomes to resolve, but surprising when you expected them to compete.
Pairs well with
- Conditional assertions — mutex only matters when conditionals exist
fs-assert-timeout— bounds the race so dismissed siblings don't linger- OOB assertions — multi-check validation per winning branch
Gotchas
fs-assert-mutex=""is invalid. The agent warns and treats it as if omitted.- React JSX boolean drop. React drops bare boolean attributes. Write
fs-assert-mutex="each", notfs-assert-mutex. - Expecting
mutex="each"to resolve multiple success siblings together. Each dismisses every other sibling on first match. If you need "bothadded-successandvisible-successto pass together," usemutex="conditions"with the same condition key, or use OOB. - Invariant and MPA compatibility. Mutex works with user triggers. Invariant-triggered assertions don't support conditional keys; MPA assertions don't support
emittedsiblings.
See also
- Conditional assertions — how condition keys work
- OOB assertions — independent secondary checks
- Patterns cookbook #6
- Assertions index