Skip to content

ix-select: internal validation throws uncaught "Input element not found" when input is unmounted (connect/teardown) #2669

Description

@skarpyak

Prerequisites

  • I have read the Contributing Guidelines.
  • I have not leaked any internal/restricted information like screenshots, videos, code snippets, links etc.

What happened?

Expected behavior:
ix-select's internal validation check should not produce an unhandled promise rejection when its internal isn't mounted.

Actual behavior:
In v5, HookValidationLifecycle calls host.getNativeInputElement() on ix-select from a fire-and-forget async callback with no .catch(). When it runs while the internal isn't mounted (right after connect, or during teardown), getNativeInputElement() rejects with Error: "Input element not found", which surfaces as an unhandled rejection.

Error: Input element not found

at Select.getNativeInputElement (ix-select.entry.js:946)
at checkIfRequiredFunction (validation-*.js:71)

(No parent frame — fired from a detached setTimeout.)

Steps to reproduce:
Render an ix-select that mounts asynchronously (e.g. ix-select-items added after an async data load).
Unmount or toggle it (e.g. disabled) shortly after.
See the uncaught Error: Input element not found in the console.
A synchronously-rendered ix-select does not reproduce it (the input ref exists before the timer fires).

Context (from published dist, v5.1.0)
The check is scheduled on a bare setTimeout in connectedCallback and not cleared in disconnectedCallback:

// dist/esm/validation-*.js
setTimeout(checkIfRequiredFunction); // no catch, not cleared on disconnect

So it can run when inputElement is null — before first render, or after teardown.

Possible directions
getNativeInputElement() could resolve null instead of rejecting (it's an internal check), and/or
checkIfRequiredFunction could .catch() it, and
disconnectedCallback could clearTimeout the scheduled check.

Impact / environment
Occurs at runtime (console noise) and fails test runners that treat unhandled rejections as failures (e.g. Vitest browser mode / Storybook interaction tests) even when assertions pass. Component: ix-select, @siemens/ix@5.1.0, Lit web components.

What type of frontend framework are you seeing the problem on?

JavaScript

On which version of the frontend framework are you experiencing the issue?

"lit": "^3.1.3"

Which version of iX do you use?

v5.1.0

Code to produce this issue.

<!doctype html>
<!--
  Reduced test case: @siemens/ix@5.1.0 ix-select
  Unhandled promise rejection "Input element not found" from
  getNativeInputElement() during connect/teardown.

  HOW TO RUN: open this file's URL over HTTP (e.g. a gist "raw" via
  https://raw.githack.com, or `npx serve` locally). ES modules do not load
  from a file:// path, so double-clicking will not work.

  Then open the browser console, click the button, and wait ~1s.
  Expected: a red "UNHANDLED REJECTION: Input element not found" appears.
-->
<html lang="en" data-ix-theme="classic-light" data-ix-color-schema="light">
  <head>
    <meta charset="UTF-8" />
    <title>ix-select unhandled rejection repro</title>
    <link
      rel="stylesheet"
      href="https://esm.sh/@siemens/ix@5.1.0/dist/siemens-ix/siemens-ix.css"
    />
  </head>
  <body>
    <p>Open the console, click the button, then wait ~1s.</p>
    <button id="toggle">Mount ix-select, then unmount after 300ms</button>
    <div id="host"></div>

    <script type="module">
      import { defineCustomElements } from 'https://esm.sh/@siemens/ix@5.1.0/loader';

      defineCustomElements();

      // Make the unhandled rejection visible on-page and in the console.
      addEventListener('unhandledrejection', (e) => {
        const msg =
          e.reason instanceof Error ? e.reason.message : String(e.reason);
        console.error('UNHANDLED REJECTION:', msg, e.reason);
        const pre = document.createElement('pre');
        pre.style.color = 'red';
        pre.textContent = 'UNHANDLED REJECTION: ' + msg;
        document.body.appendChild(pre);
      });

      const host = document.getElementById('host');

      document.getElementById('toggle').addEventListener('click', () => {
        host.innerHTML = '';

        // Mount an ix-select whose items arrive after an async tick, like a
        // data-driven dropdown.
        Promise.resolve().then(() => {
          const select = document.createElement('ix-select');
          select.setAttribute('required', '');
          select.setAttribute('label', 'Example');

          const item = document.createElement('ix-select-item');
          item.setAttribute('label', 'Option A');
          item.setAttribute('value', 'a');
          select.appendChild(item);

          host.appendChild(select);

          // Remove it shortly after mount. The validation setTimeout scheduled
          // in ix-select's connectedCallback then runs against a torn-down
          // component whose internal <input> is gone -> getNativeInputElement()
          // rejects, and nothing catches it.
          setTimeout(() => {
            host.innerHTML = '';
          }, 300);
        });
      });
    </script>
  </body>
</html>

Metadata

Metadata

Assignees

No one assigned

    Labels

    triageWe discuss this topic in our internal weekly

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions