chore: enable unawaited_futures and discarded_futures lints#1454
Merged
Conversation
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().
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.
grdsdev
approved these changes
Jun 22, 2026
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Enables the Dart linter rules
unawaited_futuresanddiscarded_futuresin the sharedsupabase_lintsconfig, and resolves every resulting violation across the packages.Why
Without these rules, a
Futurethat is created but never awaited (or wrapped inunawaited(...)) 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_futurescatches this inasyncbodies anddiscarded_futurescovers synchronous bodies, so the two together close the gap.How
Each of the ~111 flagged sites was resolved by judgment, not blanket replacement:
awaitwhere the result or errors matter and awaiting preserves behavior without changing a public signature (for exampleremoveChannel/removeAllChannels,dispose()in already-async code, andyet_another_json_isolate'sdecode/encode, which now propagate isolate-spawn failures that were previously dropped).unawaited(...)for genuine fire-and-forget calls that must not block: cleanup indispose()/closecallbacks, synchronous listener callbacks (auth state, deep links, app lifecycle), realtime socket sends/heartbeat/reconnect, the stream-pumping inpostgrest_builder.asStream()andsupabase_stream_builder, and constructor-kicked initialization.await, withunawaited(...)only where a test deliberately fires without waiting.No public API signatures were changed.
Verification
dart analyze: 0unawaited_futures, 0discarded_futures, 0 new errors/warnings across all packages.dart analyze --fatal-infos: clean.dart format lib test -l 80 --set-exit-if-changed: passes.