Skip to content

Enable no-floating-promises and make fire-and-forget explicit#1855

Draft
kmcginnes wants to merge 8 commits into
mainfrom
enable-no-floating-promises
Draft

Enable no-floating-promises and make fire-and-forget explicit#1855
kmcginnes wants to merge 8 commits into
mainfrom
enable-no-floating-promises

Conversation

@kmcginnes

Copy link
Copy Markdown
Collaborator

Description

Turns on the oxlint typescript/no-floating-promises rule (previously off) and resolves all 155 resulting violations across the codebase.

Why: localForage-backed Jotai atom writes (atomWithLocalForage) return a background persistence promise that callers dropped silently. A genuinely-swallowed persistence failure was indistinguishable from intentional fire-and-forget, hiding potential latent bugs. This rule makes ignoring a promise explicit everywhere.

The convention (two idioms by meaning):

  • fireAndForget(promise) — new helper in @/utils that attaches .catch(logger.error). Used for unawaited writes whose rejection should be observed: persisted-atom setters. A failed durable write now gets logged instead of vanishing.
  • bare void expr; — for genuinely inert promises: aborted navigate(), refetches whose errors surface via query state, self-catching async helpers, and test-state seeding.

How to read:

  • Core: packages/graph-explorer/src/utils/fireAndForget.ts (+ test) and .oxlintrc.json (rule flip). Start here.
  • Convention rationale: docs/adr/20260618-explicit-floating-promise-convention.md, with an agent-facing summary in .kiro/skills/typescript/SKILL.md.
  • Everything else is the mechanical sweep: each call site wrapped in fireAndForget(...) or void per the convention. Production setters (~46 sites) and test seeding (~109 sites).

This PR does not change the runtime behavior of persisted setters — it only makes the existing fire-and-forget explicit. Surfacing persistence failures to users (toast) is intentionally deferred.

Validation

  • pnpm checks passes (lint, format, types).
  • pnpm test passes (162 files, 1811 passed, 1 expected-fail).
  • New fireAndForget helper built test-first.

Related Issues

Check List

  • I confirm that my contribution is made under the terms of the Apache 2.0 license.
  • I have verified pnpm checks passes with no errors.
  • I have verified pnpm test passes with no failures.
  • I have covered new added functionality with unit tests if necessary.
  • I have updated documentation if necessary.

Turn on the oxlint typescript/no-floating-promises rule and resolve every
violation across the codebase. Persisted Jotai atom writes return a background
persistence promise that was previously dropped silently, hiding real failures
behind intentional fire-and-forget calls.

Add a fireAndForget helper that observes failures via logger.error, and apply
two idioms by meaning: fireAndForget for unawaited writes whose rejection should
be logged (persisted-atom setters), and bare void for genuinely inert promises
(navigation, self-catching helpers, test-state seeding). Record the convention
in an ADR and the TypeScript conventions skill.
An explicitly-ignored rejection is non-blocking by construction, so warn
better reflects its severity than error. Prefix the log so a reader knows
the rejection came from a deliberately-unawaited promise.
Use `promise.catch(logAndIgnore)` instead of `fireAndForget(promise)`. The
catch-handler form reads left-to-right and avoids wrapping each call in an
extra paren and indent level, which is clearest for the multi-line setter
blocks. Behavior is unchanged: rejections are still logged at warn level.
Where a test seeds a localForage-backed atom inside an async body or
beforeEach, await the write instead of marking it void. This fails the test
on a seeding error and removes the unhandled-rejection race at teardown.
Seeds inside the synchronous renderHookWithJotai initializeState callback stay
void, since its return is ignored and awaiting there is a no-op.
LoggerConnector.test.ts and localDb.test.ts call promise-returning writes
inside async test bodies where the result can be awaited. Await them instead
of marking void, so a failed write fails the test rather than surfacing as an
unhandled rejection at teardown.
renderHookWithState, renderHookWithJotai, and DbState.applyTo now await the
localForage-backed atom writes they seed, instead of leaving them as floating
void promises. This means a seeding failure surfaces as a test failure and the
unhandled-rejection race at teardown is gone.

Both render helpers became async and renderHookWithJotai's initializeState now
accepts an async callback, so all call sites await the helper (and their
enclosing test functions are async). Sweeps the ~35 affected test files.
LoggerConnector methods now return Promise<void> instead of hiding the
server-send promise behind a void contract. When sending a log to the server
fails, ServerLoggerConnector logs a console warning and shows a warning toast,
so the failure is no longer silent.

Consumers that fire logging fire-and-forget (the explorers and schema fetchers)
mark the now-visible promise with void; its rejection is handled inside the
connector, so floating it is inert.
Add logAndNotify(message, options?), a .catch handler that logs at warn level
and shows a warning toast, as the user-facing counterpart to logAndIgnore.

Migrate every user-facing fire-and-forget persistence write to logAndNotify so
a failed save is no longer silently swallowed: connections, schema cache, graph
session, styles, namespace prefixes, settings, and layout preferences. Styling
sites build the message with useTranslations so node/edge vocabulary reads
correctly per query engine. The background canvas icon fetch keeps logAndIgnore
since its failure is not worth interrupting the user.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant