Warning codes (E0xx / S0xx)
Every warning has a code, and each message is printed once per place. Dev mode is on automatically on
localhost, 127.0.0.1, file://, *.local, *.test, with ?dev in the module URL or
localStorage['aegis:dev'] = '1' (Aegis.dev.enable() on a production page, then reload).
window.__AEGIS_DEV__ = false silences everything; 'strict' turns every warning into a thrown AegisWarning
(error.code, error.what, error.why, error.fix). onWarn(fn) subscribes for tests.
| Code | What | Fix |
|---|---|---|
| E001 | effect() created outside a scope — it will never be cleaned up. |
Create it inside component()/mount() setup or scope.run(() => …). |
| E002 | A signal was written inside a computed(). |
Computeds must be pure — move the write into an effect or an action. |
| E003 | mount(selector) found no element. |
Check the selector or call mount() after DOMContentLoaded. |
| E004 | list() keys are missing or duplicated. |
Pass a unique key: list(items, render, { key: 'uuid' }). |
| E005 | A scope was created or run() on an already disposed scope. |
Do not reuse disposed scopes — create a new one. |
| E006 | A value in html`` sits in an unsupported position (tag name, comment, <script>/<style> body) or a property/boolean/bind: binding has more than one value. |
Put values into text or attributes; .prop=${v} takes exactly one value. |
| E007 | Invalid JSON in data-aegis-cache or jsonScript(). |
Serialize with JSON and escape </script as <\/script. |
| E008 | slot() called twice for the same selector. |
Server children are moved once — keep the returned fragment. |
| E009 | bind:value got something that is not a signal. |
bind:value=${mySignal} — pass the signal, not .value. |
| E010 | register() called twice with the same name, or with something that is not a setup function / { load }. |
Register each island once. |
| E011 | hydrate() found data-aegis="x" but nothing registered under x. The warning prints a ready-to-paste register() scaffold. |
Register it (or check the name — the message suggests close matches). |
| E012 | A reactive child in html`` returned more than 20 nodes. |
Use list(items, render, { key }) for collections. |
| E013 | clone() / show(cond, fragment) had to fall back to cloneNode — bindings would be dead. |
Pass a factory: show(cond, () => html…). |
| E014 | attach() element was never connected to the document. |
Insert the template in the same task, or call attach(el, fn) after inserting. |
| E015 | HTTP layer: unknown CSRF preset, or a response that looks like JSON but has a non-JSON Content-Type. |
Use django/rails/laravel/go or a custom { header, cookie }; send Content-Type: application/json. |
| E016 | An effect returned a Promise. | Signals read after await are not tracked — move async work into resource()/mutation()/watch(). |
| E017 | onDispose() called outside a scope. |
Call it inside setup or scope.run(). |
| E018 | IntersectionObserver/ResizeObserver/MutationObserver is missing in this environment (jsdom). |
The observer is a no-op; polyfill it in tests if you need the behaviour. |
| E019 | Lost reactivity: text(el, count.value), show(count.value > 3, …), an effect that read no signals, setErrors() with an unknown field, or a signal inside css``. |
Pass the signal or a function: text(el, count), show(() => count.value > 3, …). |
| E020 | Unknown event modifier on @event.mod, or .prevent combined with .passive. |
Supported: prevent stop self once passive capture outside window document debounce.N throttle.N enter esc space tab up down left right delete backspace ctrl meta shift alt. |
| E021 | persisted() could not read or write storage. |
The signal keeps working in memory; clear the key or fix serialize/deserialize. |
| E022 | inject(key) found nothing provided. |
provide(key, value) in a parent setup or pass a fallback: inject(key, fallback). |
| E023 | swap() did not find the select selector in the response. |
Return the fragment itself or pass { select } that matches the response. |
| E024 | adopt() template does not match the server DOM (wrong element at a path, or a text value that is not the only child). |
Keep the server partial and the template in sync; wrap text values in <span>. |
| E025 | A data-* island prop looks numeric but is passed as a string. |
Declare the type: register(name, setup, { types: { count: Number } }). |
| E026 | The setup ctx has no such key (typo in the destructuring). |
Use the suggested name from the warning; the full list is in aegis.d.ts (SetupContext). |
| E027 | A flush took many rounds — effects keep writing signals other effects depend on (ping-pong). The scheduler learns writer → reader edges: a self-write (s.value = s.value + 1 inside the effect that reads s) and a write cycle between effects are reported in the first round with the effect names, not after the round limit. |
Derive with computed() instead of writing from an effect; batch related writes; break the cycle. Applies to flush: 'micro' / 'frame' lanes too: a lane drains in one tick, a self-loop throws Infinite reactive loop in "micro" lane. |
| E028 | Zombie binding: the node left the document, but the binding keeps updating it — it leaks until the owner scope is disposed. | Render the branch through show()/list(), or dispose the binding (const off = text(el, …); off()) before dropping the node. |
| E029 | Two resource() instances fetched the same URL within a second. |
Add cache: { key } so they share one request, or lift the resource into a parent and provide() it. |
| E030 | The same URL was fetched many times within a second (a fetch loop). | Do not create a resource() inside an effect; give it a stable key; check refetchOn dependencies. |
| E031 | A Promise or a plain object was rendered as text in html`` (shows as [object Promise] / [object Object]). |
Async work goes in resource() + when(res, { data }); pick a field or JSON.stringify() for objects. |
| E032 | @event name is not a DOM event on that element (typo like @clik). |
Use the suggested name; custom events need a dash (@item-select). |
| E033 | Vue / Alpine / Angular / mustache syntax inside html`` (v-if, x-data, {{ }}) — inert here. |
show() / list() / bind:value / @click / ${} — see llms.txt "Template syntax". |
| E034 | @event got a non-function: the handler was called (@click=${save()}) or is undefined. |
Pass the function: @click=${save} or @click=${() => save(id)}; null/false skips a handler on purpose. |
| E035 | A resource URL contains undefined, null, NaN or [object …]. |
Return null from the URL function until the value is ready — a null URL skips the request. |
| E036 | html() was called as a function instead of a template tag. |
html<p>${name}</p>`` with backticks; tpl() / swap() for server HTML strings. |
| E037 | router: no route matches the URL and there is no * route. |
Add '*': () => render404() or fix the pattern / base. |
| E038 | resource({ cache }) got a non-string cache key. |
Pass a URL, an array key cache: { key: ['users', id] } or params — they are normalized. |
| E039 | staleTime is larger than cacheTime — the entry is collected while still fresh. |
Set cacheTime >= staleTime (or Infinity for reference data). |
| E040 | invalidate(key) matched no cache entry (query order, trailing slash, typo). |
Use the suggested key, the prefix form invalidate('/api/users*'), an array key or a predicate. |
| E041 | revalidateOn refetched 8+ entries at once on focus/reconnect (thundering herd). |
Raise staleTime on slow-changing data or set revalidateOn: [] where mutations already invalidate. |
| E042 | 90 %+ of revalidations returned identical data — staleTime is far below the observed change rate. |
Use the suggested staleTime from the warning / cache.explain(key).suggestedStaleTime, or staleTime: 'auto'. |
| E043 | Fewer than 20 % of prefetched responses were ever used (cache.stats().prefetch). |
Prefetch on tap instead of hover, raise the hover delay, or use a predictor. |
| E044 | A cache entry violated an internal invariant (refCount, in-flight state, patches). | Engine bug — please report the steps; cache.remove(key) clears the entry meanwhile. |
| E045 | A disposed computed() was read — its value is frozen. |
Do not dispose a computed that is still read; computed() needs no dispose (it is unsubscribed while nobody observes it). |
| E046 | watch() / until() got a plain value or a reactive object as source — it never fires. |
watch(() => state.count, cb) or pass the signal. |
| E047 | The same object reference was written back into a signal — inner mutations are invisible. | sig.update(a => [...a, x]), hold it in reactive(), or signal(v, { equals: false }). |
| E048 | ${count.value} (or title=${count.value}) inside html`` is a snapshot — inserted once, never updates. |
Pass the signal ${count} or a getter ${() => …}; a deliberate snapshot is ${count.peek()}. |
| E049 | A dialog / <dialog> opened by trap() / modal() has no accessible name. |
Put a heading inside (it becomes aria-labelledby) or set aria-label. |
| E050 | The focused element was removed by list() / show() and no neighbour or container could take focus — focus fell to <body>. |
Move focus before removing; for rows re-rendered on object replacement pass { item: 'signal' }. |
| E051 | A runtime contract of the reactive graph or the scope tree failed (subscriptions ⇔ dependencies, live computed ⇔ observed, no disposed observer in subs, no observer stranded in the queue, depth counters at rest, children ⇔ parent). Checked after every flush in dev when dev.contracts is 'strict', sampled (~64 per session) by default. |
This is an engine bug, not an application bug: report it with the steps; dev.contracts = false silences it meanwhile. |
| E052 | A mutation({ isolation: 'validate' }) body read signals through ctx.read and they changed while the request was in flight — the request was built from stale state. The optimistic patch is dropped and the touched keys are refetched. |
Read everything the request needs through ctx.read (not sig.value after an await), or drop isolation to accept last-writer-wins. |
| S002 | A URL attribute (href, src, action, formaction, …) received a value that chose the scheme (javascript:, data:, vbscript:); it was replaced by about:blank#aegis-unsafe. Decided at compile time from the static prefix: href="/u/${id}" is never checked. |
Pass a path or an https URL; wrap a vetted value in trusted(url); allow custom schemes with configure({ urlSchemes: [...] }). |
| S003 | An on* attribute got a string instead of a function — inline handler strings are code injection and are refused. |
Write @click=${fn} or onclick=${fn}. |
| S004 | An HTML sink (srcdoc, .innerHTML, .outerHTML) got a plain string. |
Render markup with html``, or pass a TrustedHTML from your policy / trusted(html) for author-vetted markup. |
| S006 | data-aegis-src points outside the origin policy (default same-origin) — the island module was not imported. |
Serve islands same-origin or configure({ islands: { src: ['https://cdn.example/'] } }). |
| S007 | An island inside [data-aegis-untrusted] is not in configure({ islands: { allow } }) — not mounted (markup from sanitized/user zones must not instantiate arbitrary islands). |
Add the island name to the allow-list if it is safe there, or move the markup out of the zone. |
| S008 | data-aegis-props points to a JSON script outside the island (or its parent) — ignored, so an injected island cannot read arbitrary page JSON. |
Put the <script type="application/json"> inside or next to the island. |
| S009 | A credential-like header (Authorization, X-Api-Key, Cookie, X-CSRF-Token) is being sent to an origin that is not trusted (dev only). |
configure({ origins: [...] }) if the origin is yours; flat configure({ headers }) and the CSRF token only go to trusted origins. |
| S010 | data-aegis-integrity did not match the fetched island module (SRI for dynamic import). |
Update the hash after every deploy (build.mjs emits dist/integrity.json). |
| S011 | Trusted Types are enforced by CSP and no server policy is configured — server HTML (swap, boost, wireForm) cannot be parsed. |
configure({ trustedTypes: { server: (html) => policy.createHTML(html) } }); the html`` literal policy is aegis (add it to trusted-types). |
| S012 | swap({ sanitize }) ran without the Sanitizer API — only script/iframe/object, on* attributes and javascript: URLs were removed. |
Sanitize on the server, or ship DOMPurify for browsers without Element.setHTML(). |
| S001 | Attempt to set __proto__ / prototype / constructor on a reactive object — blocked. |
Use a regular property name. |