Comfy-Org / Comfy-Org/ComfyUI_frontend

Enable no-throw-literal in oxlint: 13 bare-literal throws in litegraph, plus one legitimate re-throw to exempt

Open
#15,635 2 comments 0 reactions 1 assignee Claimed by @christian-byrne View on GitHub
developer experience
Dominant language
TypeScript
Stars
2k
Forks
699
Avg merge
1d 7h
Merged PRs (30d)
490

Description

## Problem

`throw 'target node is null'` produces a value with no `.stack` and no `instanceof Error`. Sentry
groups these badly, `catch (e) { e.message }` returns `undefined`, and nothing upstream can tell a
thrown string from a rejected value. oxlint ships `no-throw-literal` and this repo has it switched
off, so 14 such sites sit in `src/` today. Thirteen are bare literals that should become
`new Error(...)`. The fourteenth is a legitimate re-throw and must be exempted, not converted.

Enable the rule and fix the sites. Everything here is pre-existing on `main` and unrelated to any
open branch.

GitHub issue: https://github.com/Comfy-Org/ComfyUI_frontend/issues/15635 — unassigned, no labels.
That issue's line numbers were measured on `feature/ecs-migration` and have drifted. The numbers
below are re-measured on `origin/main` and are the ones to work from.

## Provenance

* **Found by:** lint-coverage sweep of ComfyUI_frontend, then re-verified against `origin/main` for
this ticket · 2026-08-22
* **How:** `oxlint -A all -D no-throw-literal src` executed at `origin/main` `a08a7598aa`, then every
one of the 14 reported sites opened and read
* **Why now:** the fix is one config line plus 13 mechanical conversions, and it closes a rule gap
that neither linter covers today
* **Lane rationale:** mechanical — the rule name, the exact site list and the fix shape are all
known; nothing here needs a decision
* **Dedup identity:** `fe-oxlint-no-throw-literal`. Linear dedupe run 2026-08-22 found no existing
ticket; the only prior art is BE-582 / BE-1982, which are unrelated oxlint adoption work
* **Confidence:** verified — every site below was opened and read

## Evidence

Measured with oxlint 1.77.0 at `origin/main` `a08a7598aa`:

```
npx --no-install oxlint -A all -D no-throw-literal src
```

The run reports **14 findings**. Thirteen are bare literals to convert; the fourteenth is a
legitimate re-throw to exempt. Both controls were run first, so the instrument is known live in
both directions: `-D no-console` on a litegraph file reports findings and exits 1, `-D no-debugger`
on the same file reports nothing and exits 0.

Thirteen bare-literal throws to convert:

| Site | What is there |
| --- | --- |
| `src/lib/litegraph/src/ContextMenu.ts:364` | `throw 'ContextMenu submenu needs options'` |
| `src/lib/litegraph/src/LiteGraphGlobal.ts:412` | `throw 'Cannot register a simple object, it must be a class with a prototype'` |
| `src/lib/litegraph/src/LiteGraphGlobal.ts:456` | `` throw `node type not found: ${String(type)}` `` |
| `src/lib/litegraph/src/LGraphNode.ts:2093` | `throw "LiteGraph addWidget('combo',...) requires to pass values in options: ..."` |
| `src/lib/litegraph/src/LGraphNode.ts:2904` | `if (!nodeById) throw 'target node is null'` |
| `src/lib/litegraph/src/LGraphNode.ts:2908` | `if (!target_node) throw 'target node is null'` |
| `src/lib/litegraph/src/LGraphNode.ts:3226` | `if (!target) throw 'Target Node not found'` |
| `src/lib/litegraph/src/LGraph.ts:992` | `throw 'LiteGraph: max number of nodes in a graph reached'` |
| `src/lib/litegraph/src/LGraphCanvas.ts:1708` | `if (!node) throw 'no node for color'` |
| `src/lib/litegraph/src/LGraphCanvas.ts:1770` | `if (!node) throw 'no node passed'` |
| `src/lib/litegraph/src/LGraphCanvas.ts:1950` | `throw 'Error validating LiteGraph canvas: Canvas element not found'` |
| `src/lib/litegraph/src/LGraphCanvas.ts:1992` | `` throw `Element supplied for LGraphCanvas must be a element, you passed a ${element.localName}` `` |
| `src/lib/litegraph/src/LGraphCanvas.ts:1994` | `throw "This browser doesn't support Canvas"` |

The fourteenth is **not** a defect and must not be converted:

* `src/platform/cloud/subscription/components/CreditsTile.vue:451` — `if (lastError) throw lastError`.
`lastError` is declared `let lastError: unknown` at `:441` and assigned from `catch (error)` at
`:448`. This is a deliberate re-throw of a caught error inside a coalescing refresh loop
(`refreshLatestCredits`, `:436-452`). Converting it to `new Error(...)` would destroy the original
error and its stack. oxlint flags it only because it cannot prove an `unknown` is an `Error`.

### The 13-vs-14 count, settled

The count was disputed. It was settled by opening every site at `origin/main` `a08a7598aa`, and
**13 is the number of conversions**. Two separate counting errors produced the competing 14:

1. **`CreditsTile.vue` does exist on `main`.** A claim that it does not, and that only
`browser_tests/tests/dialogs/creditsTile.spec.ts` exists, is wrong.
`git ls-tree -r --name-only origin/main | grep -i creditstile` returns three paths, including
`src/platform/cloud/subscription/components/CreditsTile.vue`. The file was opened at `:436-459`
and reads exactly as described below.
2. **A text search for bare literals returns 14, but one of them is a comment.** Grepping
`throw[[:space:]]+['"`]` across `src/` at `origin/main` returns 14 lines. The fourteenth is
`src/stores/widgetStore.ts:28`, which is the comment
`// throw "Cannot convert undefined or null to object" and break app init.` inside a guard's
explanatory block. It is not a throw. Opened and confirmed.

So both roads arrive at 13 conversions: oxlint reports 14 findings of which one
(`CreditsTile.vue:451`) is a correct re-throw, and the text search finds 14 candidate lines of which
one (`widgetStore.ts:28`) is a comment. Do not re-litigate this; the numbers below are the ones to
work from.

Config state at `origin/main` `a08a7598aa`:

* `.oxlintrc.json` has no `no-throw-literal` entry, so the rule is off. The `eslint` plugin is
already enabled in its `plugins` array, so no new dependency is needed.
* eslint does not configure `no-throw-literal` or `@typescript-eslint/only-throw-error` either, so
the rule is genuinely uncovered rather than enforced elsewhere.
* `oxlint -A all -D typescript/only-throw-error src` reports nothing, so that rule is not an
available alternative in this oxlint version. Use `no-throw-literal`.

Litegraph already imports and throws `NullGraphError` and uses `new Error(...)` in the majority of
its throw sites, so the 13 above are the inconsistent minority, not the local convention.

Attribution: `git blame` is not usable here. Local clones of this repo are shallow, so every line
older than the graft collapses onto a single unrelated commit. Use
`GET /repos/Comfy-Org/ComfyUI_frontend/commits?path=` if you need the real author.

## Acceptance criteria

- [ ] `"no-throw-literal": "error"` added to the `rules` block of `.oxlintrc.json`
- [ ] All 13 litegraph sites converted to `throw new Error(...)`, preserving the existing message
text verbatim
- [ ] `CreditsTile.vue:451` left as `throw lastError`, with a
`// oxlint-disable-next-line no-throw-literal` comment and a one-line reason. This repo already
uses `oxlint-disable` comments (e.g. `src/utils/graphTraversalUtil.test.ts:1`), so the
mechanism is established
- [ ] `npx oxlint src` exits 0 with no `no-throw-literal` findings after the change
- [ ] Any existing test that catches one of these throws and asserts on the caught **string** is
updated to assert on `error.message`. Search for the message text before converting each site;
the PR states how many such call sites were found (including zero)
- [ ] `pnpm lint`, `pnpm typecheck` and `pnpm test:unit` pass. Note that `vue-tsc` OOMs at Node's
2GB default and exits 134 while a grep filtered for `error TS` returns nothing — run it as
`NODE_OPTIONS=--max-old-space-size=8192 vue-tsc --noEmit` and report the exit code

## Out of scope

1. Enabling `no-throw-literal` in eslint as well — one linter is enough, and oxlint is the one that
already loads the `eslint` plugin
2. Any other oxlint rule currently off in `.oxlintrc.json`
3. Introducing new typed error classes, or changing which error type any site throws beyond
`Error`
4. `src/scripts/*` and `src/extensions/core/*` — both are in `.oxlintrc.json`'s `ignorePatterns`
(`:12-13`) and are therefore not scanned. Do not un-ignore them here; that is a separate
decision with a much larger blast radius
5. Changing any thrown message string

## Working notes

Everything needed is above; you do not need any other context to finish this.

* Base off `main`. Do not target `feature/ecs-migration`, and do not merge anything.
* This is a pnpm workspace with **eight** `node_modules` directories. If only the root one is
present, vitest dies at `Failed to resolve import "clsx"` at *every* commit, which turns any
before/after comparison into noise. Run `vitest run src/lib/litegraph/src/LLink.test.ts` as a
positive control before trusting any red result.
* `oxlint` exit codes are ambiguous in both directions: it exits 0 when findings are warning-level,
and exits 1 both for real errors **and** when every input path is ignored
(`No files found to lint`). Read the output, not just `$?`.

---

Original issue text as filed on 2026-08-22 (superseded by the section above, preserved in full)

The body above replaces this text. It was re-measured against `origin/main` `a08a7598aa`; the line numbers below were measured on `feature/ecs-migration` and have drifted. The count is now settled at 13 conversions plus one exemption (see "The 13-vs-14 count, settled" above). Nothing from the original has been deleted.

`no-throw-literal` is available in oxlint and switched off. Turning it on flags 14 sites in `src/`, all pre-existing. Two of them are bugs that were found by hand during ECS review last week, which is the point of filing this.

## Why it matters

`throw 'target node is null'` produces a value with no `.stack` and no `instanceof Error`. Sentry groups these badly, `catch (e) { e.message }` gives `undefined`, and nothing upstream can tell a thrown string from a rejected value.

## Measured

oxlint 1.77.0, at `5002fae1b1`:

```
$ npx --no-install oxlint -A all -D no-throw-literal src
```

| Count | Scope |
| --- | --- |
| 13 | `src/lib/litegraph/` |
| 1 | `src/platform/cloud/subscription/components/CreditsTile.vue` |
| **14** | **total** |

Sites in litegraph:

```
src/lib/litegraph/src/LGraphCanvas.ts:1716, :1778, :1958, :2000, :2002
src/lib/litegraph/src/LGraphNode.ts:2237, :3050, :3054, :3349
src/lib/litegraph/src/LiteGraphGlobal.ts:412, :456
src/lib/litegraph/src/LGraph.ts:1153
src/lib/litegraph/src/ContextMenu.ts:364
```

Count is identical at `6532665db9` (the `feature/ecs-migration` merge base) and at `5002fae1b1`, so this is entirely pre-existing and not introduced by #14246.

## Current config state

- eslint: `no-throw-literal` and `@typescript-eslint/only-throw-error` are **not configured at all** (621 rules configured, 117 enabled, 504 off, per `eslint --print-config`).
- oxlint: rule exists in the bundled `eslint` plugin, not listed in `.oxlintrc.json`, so it is off.

No new dependency is needed. This is one line in `.oxlintrc.json` plus 14 fixes.

## Suggested

Add `"no-throw-literal": "error"` to `.oxlintrc.json` and convert the 14 sites to `throw new Error(...)`. Litegraph already has `NullGraphError` and uses `new Error(...)` in the majority of its throws, so the 13 there are the inconsistent minority, not the convention.

Worth checking whether any caller depends on catching the string value before converting.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.