I'm not sure if this is intended behaviour - at least, I do not see any reason why it would be. However, this behaviour is implemented directly in the handling of parameters of RunSynchronously
. If you look at the library source code, you can see:
static member RunSynchronously (p:Async<'T>,?timeout,?cancellationToken) =
let timeout,token =
match cancellationToken with
| None -> timeout,(!defaultCancellationTokenSource).Token
| Some token when not token.CanBeCanceled -> timeout, token
| Some token -> None, token
In your case (with both timeout and a cancellation token that can be cancelled), the code goes through the last branch and ignores the timeout. I think this is either a bug or it is something that should be mentioned in the documentation.
As a workaround, you can create a separate CancellationTokenSource
to specify the timeout and link it to the main cancellation source so that the caller provides (using CreateLinkedTokenSource
). When you get OperationCancelledException
, you can then detect whether the source was an actual cancellation or a timeout:
type Microsoft.FSharp.Control.Async with
static member RunSynchronouslyEx(a:Async<'T>, timeout:int, cancellationToken) =
// Create cancellation token that is cancelled after 'timeout'
let timeoutCts = new CancellationTokenSource()
timeoutCts.CancelAfter(timeout)
// Create a combined token that is cancelled either when
// 'cancellationToken' is cancelled, or after a timeout
let combinedCts =
CancellationTokenSource.CreateLinkedTokenSource
(cancellationToken, timeoutCts.Token)
// Run synchronously with the combined token
try Async.RunSynchronously(a, cancellationToken = combinedCts.Token)
with :? OperationCanceledException as e ->
// If the timeout occurred, then we throw timeout exception instead
if timeoutCts.IsCancellationRequested then
raise (new System.TimeoutException())
else reraise()