Skip to content

chore: enable unawaited_futures and discarded_futures lints#1454

Merged
spydon merged 4 commits into
mainfrom
chore/await-futures-lint
Jun 22, 2026
Merged

chore: enable unawaited_futures and discarded_futures lints#1454
spydon merged 4 commits into
mainfrom
chore/await-futures-lint

Conversation

@spydon

@spydon spydon commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

What

Enables the Dart linter rules unawaited_futures and discarded_futures in the shared supabase_lints config, and resolves every resulting violation across the packages.

linter:
  rules:
    unawaited_futures: true
    discarded_futures: true

Why

Without these rules, a Future that is created but never awaited (or wrapped in unawaited(...)) silently drops its result. If that future throws, the exception becomes an unhandled async error instead of propagating to the caller, so failures get lost. unawaited_futures catches this in async bodies and discarded_futures covers synchronous bodies, so the two together close the gap.

How

Each of the ~111 flagged sites was resolved by judgment, not blanket replacement:

  • await where the result or errors matter and awaiting preserves behavior without changing a public signature (for example removeChannel/removeAllChannels, dispose() in already-async code, and yet_another_json_isolate's decode/encode, which now propagate isolate-spawn failures that were previously dropped).
  • unawaited(...) for genuine fire-and-forget calls that must not block: cleanup in dispose()/close callbacks, synchronous listener callbacks (auth state, deep links, app lifecycle), realtime socket sends/heartbeat/reconnect, the stream-pumping in postgrest_builder.asStream() and supabase_stream_builder, and constructor-kicked initialization.
  • Tests and examples were mostly switched to await, with unawaited(...) only where a test deliberately fires without waiting.

No public API signatures were changed.

Verification

  • dart analyze: 0 unawaited_futures, 0 discarded_futures, 0 new errors/warnings across all packages.
  • dart analyze --fatal-infos: clean.
  • dart format lib test -l 80 --set-exit-if-changed: passes.

Enable the unawaited_futures and discarded_futures linter rules in the
shared supabase_lints config so that futures are not silently dropped in
async or sync contexts, which would otherwise swallow exceptions.

Resolve all resulting violations across the packages by awaiting where
the result matters or wrapping genuine fire-and-forget calls in
unawaited().
@spydon spydon requested a review from a team as a code owner June 22, 2026 14:54
spydon added 3 commits June 22, 2026 17:18
removeChannel and removeAllChannels awaited disconnect(), which waits on
conn.ready while the socket is still connecting. Against a server that
never completes the websocket handshake this never resolves and hangs
the caller, so keep the disconnect fire-and-forget via unawaited().

The channel_test teardown had the same issue when awaiting
socket.disconnect().
The lint surfaced two spots where a future was left fire-and-forget,
silently losing exceptions:

- SharedPreferencesGotrueAsyncStorage._initialize completed its
  completer only on success, so a SharedPreferences.getInstance failure
  was lost and every later getItem/setItem deadlocked awaiting the
  completer. Complete the completer with the error so callers see it.
- _onAuthStateChange persisted and removed sessions without handling
  errors. Await them in a try/catch and log failures.
@spydon spydon merged commit c7b2598 into main Jun 22, 2026
42 checks passed
@spydon spydon deleted the chore/await-futures-lint branch June 22, 2026 17:16
spydon added a commit that referenced this pull request Jun 22, 2026
> Stacked on top of #1454. Review/merge that one first; the base will
retarget to `main` automatically once it lands.

## What

Enables the DCM rule
[`avoid-passing-async-when-sync-expected`](https://dcm.dev/docs/rules/common/avoid-passing-async-when-sync-expected/)
(removing it from the ratchet list in `supabase_lints`) and resolves the
13 resulting violations.

## Why

When an `async` callback is passed where a synchronous one is expected,
the returned `Future` is discarded, so if it throws the error is
silently lost. This is the same class of problem as the
`unawaited_futures`/`discarded_futures` lints in #1454, just at call
sites that take a callback. Enabling it closes the remaining
async-safety gap.

## How

Each of the 13 sites was made synchronous, and the inner async work was
either wrapped in `unawaited(...)` (genuine fire-and-forget) or
extracted into a dedicated method, preserving existing behavior and
error handling. Highlights:

- `realtime_channel.dart`: extracted the `joinPush.receive('ok', ...)`
body into `_handleJoinOk(...)` (the `InvalidJWTToken` setAuth handling
is preserved verbatim).
- `realtime_client.dart`: extracted reconnect into `_reconnect()`,
heartbeat timer callback made sync with `unawaited(sendHeartbeat())`.
- `supabase_client.dart`: `onAuthStateChangeSync` listener made sync
with `unawaited(_handleTokenChanged(...))`.
- `supabase_stream_builder.dart`: `onDone: controller.close` → `onDone:
() => unawaited(controller.close())` in the asyncMap/asyncExpand
reimplementations.
- Remaining sites are in test mock servers and a widget-test stub.

No public API signatures were changed.

## Verification

- `dcm analyze`: 0 `avoid-passing-async-when-sync-expected` across all
packages.
- `dart analyze`: clean, no new `unawaited_futures`/`discarded_futures`.
- Tests: realtime_client (channel/socket/mock), supabase
(mock/utilities), and supabase_flutter suites all pass.
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.

2 participants