Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support await'ing a Task without throwing #22144

Closed
stephentoub opened this issue Jun 6, 2017 · 82 comments
Closed

Support await'ing a Task without throwing #22144

stephentoub opened this issue Jun 6, 2017 · 82 comments
Assignees
Labels
api-approved API was approved in API review, it can be implemented area-System.Threading.Tasks
Milestone

Comments

@stephentoub
Copy link
Member

stephentoub commented Jun 6, 2017

EDITED 5/24/2023 by @stephentoub:
Latest proposal at #22144 (comment)

namespace System.Threading.Tasks;

public class Task
{
+    public ConfiguredTaskAwaitable ConfigureAwait(ConfigureAwaitOptions options);
}

+[Flags]
+public enum ConfigureAwaitOptions
+{
+    None = 0,
+    ContinueOnCapturedContext = 0x1,
+    SuppressExceptions = 0x2,
+    ForceYielding = 0x4,
+    ForceAsynchronousContinuation = 0x8,
+}

(I'm not sure yet if we actually need to ship ForceAsynchronousContinuation now. We might hold off if we don't have direct need in our own uses.)


EDIT: See #22144 (comment) for an up-to-date API proposal.

namespace System.Threading.Tasks;

+[Flags]
+public enum TaskAwaitBehavior
+{
+    Default = 0x0,
+    NoContinueOnCapturedContext = 0x1,
+    NoThrow = 0x2,
+}

public partial class Task
{
     public ConfiguredTaskAwaitable ConfigureAwait(bool continueOnCapturedContext);
+    public ConfiguredTaskAwaitable ConfigureAwait(TaskAwaitBehavior awaitBehavior);
}

public partial class Task<TResult>
{
     public new ConfiguredTaskAwaitable<TResult> ConfigureAwait(bool continueOnCapturedContext);
+    public new ConfiguredTaskAwaitable<TResult> ConfigureAwait(TaskAwaitBehavior awaitBehavior);
}

public partial struct ValueTask
{
     public ConfiguredValueTaskAwaitable ConfigureAwait(bool continueOnCapturedContext);
+    public ConfiguredValueTaskAwaitable ConfigureAwait(TaskAwaitBehavior awaitBehavior);
}

public partial struct ValueTask<TResult>
{
     public ConfiguredTaskAwaitable<TResult> ConfigureAwait(bool continueOnCapturedContext);
+    public ConfiguredTaskAwaitable<TResult> ConfigureAwait(TaskAwaitBehavior awaitBehavior);
}
Original post Currently there isn't a great way to await a Task without throwing (if the task may have faulted or been canceled). You can simply eat all exceptions:
try { await task; } catch { }

but that incurs the cost of the throw and also triggers first-chance exception handling. You can use a continuation:

await task.ContinueWith(delegate { }, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);

but that incurs the cost of creating and running an extra task. The best way in terms of run-time overhead is to use a custom awaiter that has a nop GetResult:

internal struct NoThrowAwaiter : ICriticalNotifyCompletion
{
    private readonly Task _task;
    public NoThrowAwaiter(Task task) { _task = task; }
    public NoThrowAwaiter GetAwaiter() => this;
    public bool IsCompleted => _task.IsCompleted;
    public void GetResult() { }
    public void OnCompleted(Action continuation) => _task.GetAwaiter().OnCompleted(continuation);
    public void UnsafeOnCompleted(Action continuation) => OnCompleted(continuation);
}
...
await new NoThrowAwaiter(task);

but that's obviously more code than is desirable. It'd be nice if functionality similar to that last example was built-in.

Proposal
Add a new overload of ConfigureAwait, to both Task and Task<T>. Whereas the current overload accepts a bool, the new overload would accept a new ConfigureAwaitBehavior enum:

namespace System.Threading.Tasks
{
    [Flags]
    public enum ConfigureAwaitBehavior
    {
        NoCapturedContext = 0x1, // equivalent to ConfigureAwait(false)
        NoThrow = 0x2, // when set, no exceptions will be thrown for Faulted/Canceled
        Asynchronous = 0x4, // force the continuation to be asynchronous
        ... // other options we might want in the future
    }
}

Then with ConfigureAwait overloads:

namespace System.Threading.Tasks
{
    public class Task
    {
        ...
        public ConfiguredTaskAwaitable ConfigureAwait(ConfigureAwaitBehavior behavior);
    }

    public class Task<TResult> : Task
    {
        ...
        public ConfiguredTaskAwaitable<TResult> ConfigureAwait(ConfigureAwaitBehavior behavior);
    }
}

code that wants to await without throwing can write:

await task.ConfigureAwait(ConfigureAwaitBehavior.NoThrow);

or that wants to have the equivalent of ConfigureAwait(false) and also not throw:

await task.ConfigureAwait(ConfigureAwaitBehavior.NoCapturedContext | ConfigureAwaitBehavior.NoThrow);

etc.

From an implementation perspective, this will mean adding a small amount of logic to ConfiguredTaskAwaiter, so there's a small chance it could have a negative imp

Alternatives
An alternative would be to add a dedicated API like NoThrow to Task, either as an instance or as an extension method, e.g.

await task.NoThrow();

That however doesn't compose well with wanting to use ConfigureAwait(false), and we'd likely end up needing to add a full matrix of options and supporting awaitable/awaiter types to enable that.

Another option would be to add methods like NoThrow to ConfiguredTaskAwaitable, so you could write:

await task.ConfigureAwait(true).NoThrow();

etc.

And of course an alternative is to continue doing nothing and developers that need this can write their own awaiter like I did earlier.

@sharwell
Copy link
Member

sharwell commented Jun 6, 2017

Rather than NoCapturedContext, I would prefer to see:

ContinueOnCapturedContext,
ConfigureAwaitBehavior.Default = ContinueOnCapturedContext

Another helpful option would be Yield, which alters the behavior in cases where the antecedent is already complete in order to force a yield.

@sharwell
Copy link
Member

sharwell commented Jun 6, 2017

💭 I really wish there was a way to return a cancelled result from an async method without throwing the exception... related to dotnet/roslyn#19652.

@stephentoub
Copy link
Member Author

I really wish there was a way to return a cancelled result from an async method without throwing the exception...

That's unrelated to this issue, though. You're talking about how you transition the returned Task to be in a canceled state. This issue is about the consuming side, regardless of how the Task-returning method was implemented... it may not have been using async/await at all.

@willdean
Copy link
Contributor

willdean commented Jun 6, 2017

I like the composable style of await task.ConfigureAwait(true).NoThrow()

Could we then have more clearly-named alternatives to ConfigureAwait(bool) which didn't need a bool?

To someone who hasn't been completely steeped in TPL, etc. from the outset I don't think ConfigureAwait(false) is intuitive at all - does "false" mean it isn't configured? Does it mean it isn't awaiting? (Rhetorical, I don't need an explanation myself)

I know a renamed ConfigureAwait alternative isn't the subject of this issue, but could it go hand-in-hand with this change? What might a good name for 'ConfigureAwait(false)' be (assuming there's any agreement that it could be improved)

@stephentoub
Copy link
Member Author

Rather than NoCapturedContext, I would prefer to see

But that means that code like:

await task.ConfigureAwait(ConfigureAwaitBehavior.NoThrow);

would end up behaving like continueOnCapturedContext==false, and thus not incorporating the default for that option.

@Clockwork-Muse
Copy link
Contributor

To someone who hasn't been completely steeped in TPL, etc. from the outset I don't think ConfigureAwait(false) is intuitive at all - does "false" mean it isn't configured? Does it mean it isn't awaiting?

...which is the exact reason Clean Code (and a lot of similar books/posts) recommends against them.

And if I ever get around to finishing that writeup of the language idea I have, raw Boolean parameters would be explicitly disallowed, in favor of some encapsulating value (like a shortcut enum declaration or something)

@svick
Copy link
Contributor

svick commented Jun 6, 2017

A very similar issue for synchronous waiting: https://github.com/dotnet/corefx/issues/8142.

@svick
Copy link
Contributor

svick commented Jun 6, 2017

I think the question here is whether it's better to have clean and simple API or whether it's better if the code that uses the API is simple and short.

With task.ConfigureAwait(ConfigureAwaitBehavior.NoCapturedContext | ConfigureAwaitBehavior.NoThrow) the API is simple, easy to understand and easy to extend, but the code is very verbose (though using static can improve that somewhat).

On the other hand, with something like task.NoCapturedContext().NoThrow(), the code is simpler and much shorter, but the API becomes much more complicated (two methods on Task and the same two methods on ConfiguredTaskAwaitable).

@benaadams
Copy link
Member

benaadams commented Jun 7, 2017

Change ConfigureAwaitBehavior to AwaitBehavior?

task.ConfigureAwait(AwaitBehavior.NoCapturedContext | AwaitBehavior.NoThrow)

@willdean
Copy link
Contributor

willdean commented Jun 7, 2017

On the other hand, with something like task.NoCapturedContext().NoThrow(), the code is simpler and much shorter, but the API becomes much more complicated (two methods on Task and the same two methods on ConfiguredTaskAwaitable).

If that's really all the choice is, then it seems to me one should almost always favour putting the complexity in the framework rather than the consuming app - otherwise what's the framework for?

If everything has to be duplicated on Task and ConfiguredTaskAwaitable then that's a real pain and might be a showstopper, but within a single class there can't be much to choose in terms of complexity/LoC between an API with a method which tears up a bitfield enum of loosely-related flags and calls helpers for each flag vs. an API which exposes the individual helpers directly.

@davidfowl
Copy link
Member

Where did we land on this API wise? Do we like the ConfigureAwait approach or the NoThrow extension method (I prefer that).

@stephentoub
Copy link
Member Author

I prefer the ConfigureAwait approach, e.g. await task.ConfigureAwait(ConfigureAwaitBehavior.NoThrow | ConfigureAwaitBehavior.NoCapturedContext);, rather than having lots of different methods for all the various ways we might want to support awaiting and that all need to compose in some fashion (this is why the method is called ConfigureAwait today, because the idea was it could be extended in this manner in the future). This also ties in with #27723, for example, where we'd have an overload that would let you do await task.ConfigureAwait(timeout), an overload that would let you pass a cancellation token, an overload that would let you pass a timeout and a cancellation token and a ConfigureAwaitBehavior, etc. It all ends up just being about configuring the await.
#32030

@eiriktsarpalis, are you still driving the design of this?

@eiriktsarpalis
Copy link
Member

I started working on a prototype a few months back, but it got dropped on the floor. I'll see if I can revive it.

@ReubenBond
Copy link
Member

ReubenBond commented Jan 14, 2021

A developer might want one of several behaviors:

  1. Signal completion but swallow exceptions
  2. Signal completion and return the result of the task but swallow exceptions and return default if the task was canceled/faulted
  3. Ensure that TaskScheduler.UnobservedTaskExeption does not fire if this task's failure is not observed
  4. Signal completion, exceptions, and cancellation without throwing for the latter two.

To illustrate, if a developer writes var result = await task.ConfigureAwait(ConfigureAwaitBehavior.NoThrow) where task is Task<string>, what would result be in the case that task failed? I suppose result may be null since we can't dependently type the return value based on the ConfigureAwaitBehavior value. In the case of await task.NoThrow(), it could be decided that it acts like Task and has no result value.

In Orleans, we often want behavior 3, and have an .Ignore() extension method for that.

If we had something similar to Rx's Observable.Materialize<TSource> method then we could gracefully await completion, exceptions, or results without throwing. Eg calling it .AsNotification() for exposition purposes:

TaskResult<T> result = await task.AsNotification();

return result switch
{
 (Canceled, _, _) => null, // Ignore if the task was canceled.
 (Faulted, _, ex) => await task, // throw if the task faulted
 (Completed, value, _) => value,
  _ => throw new NotSupportedException("...")
};

// Given deconstruction, it could be:
var (status, value, exception) = await task.AsNotification();

That doesn't satisfy behavior 3, but it satisfies behavior 1, 2, and 4, I believe.

@stephentoub
Copy link
Member Author

if a developer writes var result = await task.ConfigureAwait(ConfigureAwaitBehavior.NoThrow) where task is Task, what would result be in the case that task failed?

It would be default(T). With NoThrow, you have to check the status of task afterwards to know whether it was successful, faulted, or canceled, and you'd only use the result if it was valid.

@sharwell
Copy link
Member

I prefer the ConfigureAwait approach

I also prefer this approach. Considering how clean it is, even if it starts out slower in some sense I'd be interested to rule out JIT improvements targeting common patterns before letting it impact the API.

@ReubenBond
Copy link
Member

If a developer writes _ = task.ConfigureAwait(ConfigureAwaitBehavior.NoThrow) without awaiting, would an exception still bubble up to TaskScheduler.UnobservedTaskException?

@stephentoub
Copy link
Member Author

stephentoub commented Jan 14, 2021

would an exception still bubble up to TaskScheduler.UnobservedTaskException?

We can decide, but I think the answer should be "yes". If someone doesn't want that, they just check task.Exception, just as they can today.

@sharwell
Copy link
Member

would an exception still bubble up to TaskScheduler.UnobservedTaskException

My instinct here is yes. Normally the await operation observes the exception for a faulted task, but ConfigureAwaitBehavior.NoThrow means it does not. Code is expected to check the task status separately in this case, and failure to do so would result in an UnobservedTaskException.

@stephentoub
Copy link
Member Author

If a developer writes _ = task.ConfigureAwait(ConfigureAwaitBehavior.NoThrow) without awaiting

Oh, wait, "without awaiting"? If there's no await, the above is entirely a nop.

@willdean
Copy link
Contributor

willdean commented Jan 14, 2021

A little bit of bike-shedding, but should the enum be AwaitBehavior rather than ConfigureAwaitBehavior?

After all, it's describing the behavior of the await, not the behavior of the the ConfigureAwait function - the latter is just there to configure the behavior.

@Clockwork-Muse
Copy link
Contributor

.... Side note: If we ever get a Result<T, TError> union, are we still anticipating needing this? The intent being that exceptions would very much be "fatal" errors, whereas "ignore the small stuff" would be handled by returning the Result union.

@GSPP
Copy link

GSPP commented Jan 19, 2021

would an exception still bubble up to TaskScheduler.UnobservedTaskException?

We can decide, but I think the answer should be "yes". If someone doesn't want that, they just check task.Exception, just as they can today.

My intuition would be "no" because the developer already consciously attended to this task and decided in his head how he wants to deal with it. When UnobservedTaskException was introduced, it was meant as a debugging aid to flag likely bugs where important things are dropped on the floor. If a task is awaited I'd see this as a strong heuristic sign that the developer dealt with that task.

@stephentoub
Copy link
Member Author

My intuition would be "no" because the developer already consciously attended to this task and decided in his head how he wants to deal with it.

I don't believe we know that just from being told the await shouldn't throw. In my experience, such a request often comes from wanting to join and then handle failures in some special way and not wanting to pay the cost of a throw/catch in order to handle it. On top of that, the exception really isn't being observed, so it's arguably also more correct to not treat the exception as observed.

@stephentoub
Copy link
Member Author

In the case 3 (enumerator.MoveNextAsync) I have included a detailed explanation about why the status of this task is irrelevant.

FWIW, I disagree with the assertions in that comment that the exception is irrelevant. First, an action the user's code initiated failed with an arbitrary exception; from my perspective, it shouldn't just be eaten by the implementation because the user happened to stop enumerating (and, yes, while exceptions are discouraged from Dispose, sometimes it's the lesser evil). Second, the unobserved exception is then likely going to raise the TaskScheduler.UnobservedTaskException event, assuming the operation is actually backed by a task.

I realize this isn't the main point of the discussion, nor do we need to debate the semantics of an API we're not going to ship. I'm simply trying to highlight that it's important we factor in ValueTask.

I hope that my contribution here helped the forward progress of this API, and I didn't cause unwillingly a regression.

Thanks for the discussion.

@theodorzoulias
Copy link
Contributor

theodorzoulias commented Nov 28, 2022

@stephentoub some time ago I had a long thought about what to do with a faulted enumerator.MoveNextAsync in the finally block of an iterator. Eventually I came to the firm conclusion that the exception should be suppressed. Since then I have applied this wisdom to quite a lot of implementations (example), so now I am a bit biased towards sticking with it. 😃

Regarding the effect of the NoThrow on the TaskScheduler.UnobservedTaskException event, it was also mentioned in an earlier comment by Reuben Bond. Personally I wouldn't use this event for anything else than logging, and the level of logging would be "Warning" at most. I wouldn't mind seeing a casual warning in my logs about an error that was suppressed by a NoThrow awaiting in the finally block of my iterator. Actually this seems like the perfect usage scenario for this event: reporting occasional transient errors that don't affect the normal flow of the application.

@stephentoub
Copy link
Member Author

stephentoub commented May 24, 2023

I keep coming across places where this would be valuable, and I'd like to get a solution into .NET 8. I think we've been making this too complicated...

We don't need to support ValueTasks, which are problematic because you can't look at them after you await them, and so any model for ValueTask would need to entail returning exception information, which is unfamiliar in the model; we can instead just say "if you need this and you have a ValueTask, call AsTask first". Yes, it may allocate, but the use cases where you need this are more niche, you can check first whether it already completed, and AsTask for something that hasn't yet completed will only allocate if it's backed by an IValueTaskSource, which is itself currently problematic for such configuration because it doesn't provide a way currently to get at the Exception without throwing it.

Further, we don't need to support TResults. Previous concerns with TResult includes not knowing whether you can actually trust the returned TResult, and any nullability annotations possibly being wrong. But Task<TResult> derives from Task, so any support for Task "just works" for Task<TResult>, and in particular without violating anything about the TResult's nature.

So, if we just expose this support for Task, we address the 99% case and we can make that work well.

Thus my proposal is simply this:

namespace System.Threading.Tasks;

public class Task
{
+    public ConfiguredTaskAwaitable ConfigureAwait(ConfigureAwaitOptions options);
}

+[Flags]
+public enum ConfigureAwaitOptions
+{
    /// <summary>No options specified.</summary>
    /// <remarks>
    /// This behaves identically to using <see cref="Task.ConfigureAwait(bool)"/> with a <see langword="false"/> argument.
    /// </remarks>
+   None = 0,

    /// <summary>
    /// Attempt to marshal the continuation back to the original <see cref="SynchronizationContext"/> or
    /// <see cref="TaskScheduler"/> present at the time of the await.
    /// </summary>
    /// <remarks>
    /// If there is no such context/scheduler, or if this option is not specified, the thread on
    /// which the continuation is invoked is unspecified and left up to the determination of the system.
    /// </remarks>
+   ContinueOnCapturedContext = 0x1,

    /// <summary>
    /// Avoids throwing an exception at the completion of an await on a <see cref="Task"/> that ends
    /// in the <see cref="TaskStatus.Faulted"/> or <see cref="TaskStatus.Canceled"/> state.
    /// </summary>
+   SuppressThrowing = 0x2,

    /// <summary>
    /// Forces an await on an already completed <see cref="Task"/> to behave as if the <see cref="Task"/>"/>
    /// wasn't yet completed, such that the current asynchronous method will be forced to yield its execution.
    /// </summary>
+   ForceYielding = 0x4,

    /// <summary>
    /// Forces any continuation created for this await to be queued asynchronously as part of the
    /// antecedent <see cref="Task"/>'s completion.
    /// </summary>
    /// <remarks>
    /// This flag is only relevant when awaiting a <see cref="Task"/> that hasn't yet completed,
    /// as it impacts how the continuation is stored in the <see cref="Task"/>.  When the <see cref="Task"/>
    /// eventually completes, if the <see cref="Task"/>'s completion would have otherwise synchronously
    /// invoked the continuation as part of its completion routine, this flag will instead force it
    /// to be queued for asynchronous execution.  This is a consumption-side equivalent to the
    /// <see cref="TaskCreationOptions.RunContinuationsAsynchronously"/> flag, which allows the producer
    /// of a <see cref="Task"/> to specify that all of that <see cref="Task"/>'s continuations must be
    /// queued rather than allowing any to be synchronously invoked.
    /// </remarks>
+   ForceAsynchronousContinuation = 0x8,
+}

Notes:

  • Whether we need a different return type for this ConfigureAwait overload will depend on implementation and perf testing. Hopefully we can get away with just using the same type as the one that exists. If we can't, we'd add a new type with the same shape as ConfiguredTaskAwaitable but instead named ConfiguredWithOptionsTaskAwaitable (name can obviously be debated).
  • As was previously proposed by Pent, we could make this a new awaitable whose GetResult would return the original Task. That might help some cases, but I'm concerned it could be confusing (especially in a case where you have a Task<Task>).
  • This might be confusing for someone who actually cares about the current context, such as in client UI, and adds a .ConfigureAwait where they didn't previously have one in order to suppress exceptions and neglects to also add ContinueOnCapturedContext. But to change that, we'd need to invert the name of ContinueOnCapturedContext.

Examples:

// Wait for a task to complete or a timeout, then see whether the task was successful
await task.WaitAsync(TimeSpan.FromSeconds(10)).ConfigureAwait(ConfigureAwaitOptions.SuppressExceptions);
if (task.IsCompleted) { ... } else { ... }

// Queue the remainder of the method to the thread pool
await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceAsynchronousCompletion);

@stephentoub stephentoub added api-ready-for-review API is ready for review, it is NOT ready for implementation and removed api-needs-work API needs work before it is approved, it is NOT ready for implementation labels May 24, 2023
@bartonjs
Copy link
Member

I can't think (off the top of my head) of any other place where we have a new "override" that changes return type where the "base" method also has overloading. I anticipate confusion... do you anticipate that said confusion will be low?

For Task this is easy... you now see two ConfigureAwait(...) methods, one takes a boolean, the other takes a Flags enum (the original design finally manifests as reality), they both return a ConfiguredTaskAwaitable (unless details say otherwise... but even then they're isomorphic).

For Task<TResult>, the ConfigureAwait(bool) gets new'd to change the return type to a non-voiding awaiter, so foo = await bar.ConfigureAwait(false); is legal, but foo = await bar.ConfigureAwait(ConfigureAwaitOptions.ForceAsynchronousCompletion); is not. I understand why functionally the "voiding" awaiter is better, but wonder if the confusion means that the new method should get a different name, and not be part of the logical method group.

I don't have a better name, so I'm unfortunately just doing the "I'm not sure this is good... please solve it." It might be the best answer and we're OK with it... I'm just sharing my usability/confusion reaction before this comes up in a meeting.

@stephentoub
Copy link
Member Author

We have multiple places where overloads return different types, such that changing arguments changes the return type and this can make it no longer assignable to what it was previously. In this case I'm seeing it as a benefit.

@bartonjs
Copy link
Member

bartonjs commented Jun 1, 2023

Video

In discussion we feel that it's valid to add this to Task<T> as well as Task, but that the ConfigureAwait method on Task<T> will throw an ArgumentException (or type derived therefrom) if SuppressException is asserted.

We may consider the ValueTask family in later releases.

We should also provide an analyzer that detects the call to Task<T>.ConfigureAwait asserting the SuppressException flag, and giving an explicit a priori Warning (category: Usage)

namespace System.Threading.Tasks;

public partial class Task
{
    public ConfiguredTaskAwaitable ConfigureAwait(ConfigureAwaitOptions options);
}

public partial class Task<TResult>
{
    // throws if ConfigureAwaitOptions.SuppressExceptions is asserted (or any unknown value, of course)
    public new ConfiguredTaskAwaitable<TResult> ConfigureAwait(ConfigureAwaitOptions options);
}

[Flags]
public enum ConfigureAwaitOptions
{
    None = 0,
    ContinueOnCapturedContext = 0x1,
    SuppressExceptions = 0x2,
    ForceYielding = 0x4,
    ForceAsynchronousContinuation = 0x8,
}

@bartonjs bartonjs added api-approved API was approved in API review, it can be implemented and removed api-ready-for-review API is ready for review, it is NOT ready for implementation labels Jun 1, 2023
@eiriktsarpalis
Copy link
Member

the ConfigureAwait method on Task<T> will throw an ArgumentException

@bartonjs Is ArgumentException or NotSupportedException more appropriate here?

@bartonjs
Copy link
Member

bartonjs commented Jun 2, 2023

Largely I wrote down ArgumentException because it's what @stephentoub said. But, off the top of my head, of all the Exception types, ArgumentOutOfRangeException seems the most appropriate: "The exception that is thrown when the value of an argument is outside the allowable range of values as defined by the invoked method."

NotSupportedException says "The exception that is thrown when an invoked method is not supported, or when there is an attempt to read, seek, or write to a stream that does not support the invoked functionality." The "or when..." is really "we should have used InvalidOperationException on those Stream members, but didn't, so let's backform this and say it's OK". So, NSE is "the method, in its entirety, isn't supported... it probably got here via an interface (or legacy netfx compat)". The derived PlatformNotSupportedException would make contextual sense if the flag worked on some OSes but not all...

So, really, yeah, it's just AOORE, or some other ArgumentException, because it's "this method doesn't support this argument value, go fix your calling code"

@stephentoub
Copy link
Member Author

Fixed in #87067

@TonyValenti
Copy link

@stephentoub Thank you! I will finally be able to Task.Delay safely!

@stephentoub
Copy link
Member Author

Thank you! I will finally be able to Task.Delay safely!

You're welcome. By "safely", I assume you mean you were passing it a cancellation token and were having to try/catch(OperationCanceledException) and now you don't have to?

@davidfowl
Copy link
Member

cc @BrennanConroy

@stephentoub
Copy link
Member Author

cc @BrennanConroy

I'm planning to submit a pr to aspnetcore once it moves to a new enough runtime.

@TonyValenti
Copy link

Yep!

Thank you! I will finally be able to Task.Delay safely!

You're welcome. By "safely", I assume you mean you were passing it a cancellation token and were having to try/catch(OperationCanceledException) and now you don't have to?

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api-approved API was approved in API review, it can be implemented area-System.Threading.Tasks
Projects
None yet
Development

No branches or pull requests