I am using async I/O to communicate with an HID device, and I would like to throw a catchable exception when there is a timeout. I've got the following read method:
public async Task<int> Read( byte[] buffer, int? size=null )
{
size = size ?? buffer.Length;
using( var cts = new CancellationTokenSource() )
{
cts.CancelAfter( 1000 );
cts.Token.Register( () => { throw new TimeoutException( "read timeout" ); }, true );
try
{
var t = stream.ReadAsync( buffer, 0, size.Value, cts.Token );
await t;
return t.Result;
}
catch( Exception ex )
{
Debug.WriteLine( "exception" );
return 0;
}
}
}
The exception thrown from the Token's callback is not caught by any try/catch blocks and I'm not sure why. I assumed it would be thrown at the await, but it is not. Is there a way to catch this exception (or make it catchable by the caller of Read())?
EDIT: So I re-read the doc at msdn, and it says "Any exception the delegate generates will be propagated out of this method call."
I'm not sure what it means by "propagated out of this method call", because even if I move the .Register() call into the try block the exception is still not caught.
Exception handling for callbacks registered with CancellationToken.Register()
is complex. :-)
If the cancellation token is cancelled before the cancellation callback is registered, the callback will be executed synchronously by CancellationToken.Register()
. If the callback raises an exception, that exception will be propagated from Register()
and so can be caught using a try...catch
around it.
This propagation is what the statement you quoted refers to. For context, here's the full paragraph that that quote comes from.
If this token is already in the canceled state, the delegate will be run immediately and synchronously. Any exception the delegate generates will be propagated out of this method call.
"This method call" refers to the call to CancellationToken.Register()
. (Don't feel bad about being confused by this paragraph. When I first read it a while back, I was puzzled, too.)
When the token is cancelled by calling this method, cancellation callbacks are executed synchronously by it. Depending on the overload of Cancel()
that's used, either:
AggregateException
that is propagated out of Cancel()
.Cancel()
(not wrapped in an AggregateException
) and any unexecuted cancellation callbacks will be skipped.In either case, like CancellationToken.Register()
, a normal try...catch
can be used to catch the exception.
This method starts a countdown timer then returns. When the timer reaches zero, the timer causes the cancellation process to run in the background.
Since CancelAfter()
doesn't actually run the cancellation process, cancellation callback exceptions aren't propagated out of it. If you want to observer them, you'll need to revert to using some means of intercepting unhandled exceptions.
In your situation, since you're using CancelAfter()
, intercepting the unhandled exception is your only option. try...catch
won't work.
To avoid these complexities, when possible don't allow cancellation callbacks to throw exceptions.
I personally prefer to wrap the Cancellation logic into it's own method.
For example, given an extension method like:
public static async Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken)
{
var tcs = new TaskCompletionSource<bool>();
using (cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).TrySetResult(true), tcs))
{
if (task != await Task.WhenAny(task, tcs.Task))
{
throw new OperationCanceledException(cancellationToken);
}
}
return task.Result;
}
You can simplify your method down to:
public async Task<int> Read( byte[] buffer, int? size=null )
{
size = size ?? buffer.Length;
using( var cts = new CancellationTokenSource() )
{
cts.CancelAfter( 1000 );
try
{
return await stream.ReadAsync( buffer, 0, size.Value, cts.Token ).WithCancellation(cts.Token);
}
catch( OperationCanceledException cancel )
{
Debug.WriteLine( "cancelled" );
return 0;
}
catch( Exception ex )
{
Debug.WriteLine( "exception" );
return 0;
}
}
}
In this case, since your only goal is to perform a timeout, you can make this even simpler:
public static async Task<T> TimeoutAfter<T>(this Task<T> task, TimeSpan timeout)
{
if (task != await Task.WhenAny(task, Task.Delay(timeout)))
{
throw new TimeoutException();
}
return task.Result; // Task is guaranteed completed (WhenAny), so this won't block
}
Then your method can be:
public async Task<int> Read( byte[] buffer, int? size=null )
{
size = size ?? buffer.Length;
try
{
return await stream.ReadAsync( buffer, 0, size.Value, cts.Token ).TimeoutAfter(TimeSpan.FromSeconds(1));
}
catch( TimeoutException timeout )
{
Debug.WriteLine( "Timed out" );
return 0;
}
catch( Exception ex )
{
Debug.WriteLine( "exception" );
return 0;
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With