Element stays registered when a constructor raises after `Element.__init__`
Description
Element.__init__ commits a new instance to client.elements, appends it to the parent slot and enqueues it in the outbox. If any constructor frame above it raises afterwards, nothing is rolled back: the half-built element stays in the tree, gets rendered, and shows up in every later traversal (ElementFilter, User.should_see, event dispatch through client.elements).
We have fixed four instances by hand, each time by moving the raising statement in front of super().__init__() and leaving a NOTE about the ordering: #6271 (mixins/cancelable_wait_element.py), #6282 (codemirror/line_anchors.py), #6323 (sub_pages.py) and #6330 (select.py). Those were the cases where the leftover element was also missing attributes, so any page traversal crashed with an AttributeError.
Two more cases are still open. They do not crash, but they leave a ghost element on the page:
ui.html('<script>...</script>')raisesValueErrorfrom_handle_content_change, which runs inside thecontentsetter after registration.ui.image(Path('missing.png'))(and every otherSourceElement) raisesFileNotFoundErrorfrom_set_props, which runs inside thesourcesetter after registration.
Both checks live in setters that are shared with set_content() / set_source(), so the "validate before super()" pattern would mean duplicating them. Per-element reordering also cannot cover a raise inside an inherited constructor downstream of the registration, or user-defined subclasses that raise after super().__init__().
Proposal
Handle it once in Element instead: if construction fails after registration, unregister the element again (remove from client.elements, the parent slot's children and the outbox), including any children the failed constructor already created. This cannot reuse delete() / Client.remove_elements(), because _handle_delete() overrides assume a fully built element. With a general fix in place the four NOTEs and the ordering constraints can go.
Minimal example
from nicegui import ui
@ui.page('/')
def page():
try:
ui.html('<script>alert(1)</script>')
except ValueError:
pass
ui.label('page still works')
ui.run()The page renders an empty Html element in front of the label, and ElementFilter(kind=ui.html) finds it.
Source: zauberzeug/nicegui