Reputation: 17939
To simplify my scenario, let's suppose I have this simple code:
let someCondition = false
let SomeFuncThatThrows () =
async {
if someCondition then
raise <| InvalidOperationException()
return 0
}
let DoSomethingWithFoo (foo: int) =
Console.WriteLine (foo.ToString())
let SomeWrapper () =
async {
let! foo = SomeFuncThatThrows()
DoSomethingWithFoo foo
}
[<EntryPoint>]
let main argv =
Async.RunSynchronously (SomeWrapper ())
0
When executing it, it obviously just prints "0". However, some day, circumstances change, and some external factor makes someCondition
become true
. To prevent the program to crash in this scenario, I want to handle the exception. Then for an F# newbie it's easy to change SomeWrapper adding a try-with block, which most people would think that works:
let SomeWrapper () =
async {
let! foo =
try
SomeFuncThatThrows()
with
| :? InvalidOperationException ->
Console.Error.WriteLine "aborted"
Environment.Exit 1
failwith "unreachable"
DoSomethingWithFoo foo
}
However, this above doesn't work (the exception is still unhandled), because SomeFuncThatThrows returns a successful result: an Async<int>
element. What throws an exception is the let! foo =
bit because it awaits the async workload.
However, if you want to change SomeWrapper to fix the exception handling, many may think this is possible:
let SomeWrapper () =
async {
let foo =
try
let! fooAux = SomeFuncThatThrows()
fooAux
with
| :? InvalidOperationException ->
Console.Error.WriteLine "aborted"
Environment.Exit 1
failwith "unreachable"
DoSomethingWithFoo foo
}
But no, the compiler is not happy, as it signals the following error:
/.../Program.fs(17,17): Error FS0750: This construct may only be used within computation expressions (FS0750) (SomeProject)
Then, it seems the only way I could fix it is this way:
let SomeWrapper () =
async {
try
let! foo = SomeFuncThatThrows()
DoSomethingWithFoo foo
with
| :? InvalidOperationException ->
Console.Error.WriteLine "aborted"
Environment.Exit 1
failwith "unreachable"
}
However, I'm not 100% happy with this solution, because the try-with is too wide, as it also covers the call to DoSomethingWithFoo function, which I wanted to leave outside the try-with block. Any better way to fix this without writing non-idiomatic F#? Should I report the compiler error as a feature-request in Microsoft's F# GitHub repo?
Upvotes: 2
Views: 246
Reputation: 243051
The answer from @nilekirk works and encodes directly the logic that you were looking for, but as you noted in the comments, it is a fairly complex syntactic structure - you need a nested async { .. }
expression.
You could extract the nested async
block into a separate function, which makes the code much more readable:
let SafeSomeFunc () = async {
try
return! SomeFuncThatThrows()
with
| :? InvalidOperationException ->
Console.Error.WriteLine "aborted"
Environment.Exit 1
return failwith "unreachable"
}
let SomeWrapper2 () = async {
let! foo = SafeSomeFunc ()
DoSomethingWithFoo foo
}
Here, we actually need to put some return value into the with
branch.
Upvotes: 0
Reputation: 3784
Any better way to fix this without writing non-idiomatic F#?
In idiomatic F# and functional code, we try to get rid of using exceptions and side-effects as much as possible.
Environment.Exit
is a big side-effect, don't use it.
If SomeFuncThatThrows()
must be able to throw exception (because e.g., you cannot modify its source code). Then try to wrap it inside a safe function which returns an Option
value and use this function instead.
Your whole code can be rewritten as:
let someCondition = true
let SomeFuncThatThrows () =
async {
if someCondition then
raise <| InvalidOperationException()
return 0
}
let SomeFunc () =
async {
try
let! foo = SomeFuncThatThrows()
return Some foo
with _ ->
return None
}
let DoSomethingWithFoo (foo: int) =
Console.WriteLine (foo.ToString())
let SomeWrapper () =
async {
match! SomeFunc() with
| Some foo -> DoSomethingWithFoo foo
| None -> Console.Error.WriteLine "aborted"
}
[<EntryPoint>]
let main argv =
Async.RunSynchronously (SomeWrapper ())
0
Upvotes: 1
Reputation: 2383
You can wrap the call to SomeFuncThatThrows
in a new async
that contains a try...with
:
let SomeWrapper () =
async {
let! foo =
async {
try
return! SomeFuncThatThrows()
with
| :? InvalidOperationException ->
Console.Error.WriteLine "aborted"
Environment.Exit 1
return failwith "unreachable"
}
DoSomethingWithFoo foo
}
Upvotes: 2