Skip to content

FCE023: guard against async lambdas binding to Outcome.Try's synchronous overloads (async-void footgun) #267

Description

@Reefact

Context

Follow-up from #265 (the Outcome.Try feature). Codex flagged this on the synchronous side-effecting overload Outcome.Try<TException>(Action operation, …) — first as P2, then escalated to P1 on the deep review.

Problem

A parameterless async lambda binds to the Action parameter as async void:

Outcome.Try<InvalidOperationException>(async () => { await SaveAsync(); /* throws */ }, Map);

Overload resolution picks Action because there is no token-less async overload. The Action returns at the first await, so Try observes no exception, returns Success, and any exception raised after the first await escapes the catch out-of-band (an unobserved async void exception, which typically crashes the process). The same shape silently fire-and-forgets a task for () => ReturnsTask().

The value overloads are not affected: an async () => … returning Task<T> does not bind to Func<T> (compile error), and the token-threading async overloads (Func<CancellationToken, Task> / Func<CancellationToken, Task<T>>) require an argument, so a parameterless async lambda misses them and falls to Action.

Options to weigh in this follow-up

  1. Analyzer (the originally-planned FCE023). Flag an async lambda passed to a synchronous Outcome.Try overload and point to the token overload. Keeps the public API narrow; consistent with the FCE019–FCE022 usage-guard family. Severity to decide (Warning surfaces it; Error blocks the build where the analyzers run).
  2. Token-less async overloads. Add Func<Task> / Func<Task<T>> (delegating to the token overloads with default). Async lambdas then bind there and are awaited + mapped — a structural fix that also covers the fire-and-forget case, at the cost of two more public overloads and some tension with the "narrow Try" design.

These are not mutually exclusive, but option 2 largely obviates the analyzer for the lambda case.

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions