If you want to represent issues with the data or the query parameters, such as not finding a particular record, as being distinct from other types of failures, such as not connecting to the database or hitting an unhandled exception, you can create a discriminated union to represent your expected data/query problems explicitly. You can then return that DU instead of string as the data for the Error case, and you won't really need the Failure case. Consider something like this:
type SqlError =
| NoMatchingRecordsFound of SqlParameter list
| CouldNotConnectToDatabase
| UserDoesNotHaveQueryPermissions
| UnhandledException of exn
I would suggest taking a look at Railway-Oriented Programming and following the pattern of defining a discriminated union for your different error cases. You can still include an error message in that DU, but I would suggest using explicit cases for all your different expected failures, and then having an "UnhandledException" or similar case for your unexpected errors, perhaps with an exn for the data.
If you're interested, I have a library on GitHub/NuGet that puts all the ROP stuff together and adds interoperability with the Task, Async, and Lazy types in one computation builder, so you don't have to include all that code in your own project.
EDIT
Here's a complete example of doing it ROP-style (using the linked framework):
open FSharp.Control
open System.Linq
// Simulate user table from type-provider
[<AllowNullLiteral>]
type User() =
member val Id = 0 with get,set
member val Name = "" with get,set
// Simulate a SQL DB
let users = [User(Id = 42, Name = "The User")].AsQueryable()
// Our possible events from the SQL query
type SqlEvent =
| FoundUser
| UserIdDoesNotExist of int
| CouldNotConnectToDatabase
| UnhandledException of exn
// Railway-Oriented function to find the user by id or return the correct error event(s)
let getUserById id =
operation {
let user =
query {
for user in users do
where (user.Id = id)
select user
headOrDefault
}
return!
if user |> isNull
then Result.failure [UserIdDoesNotExist id]
else Result.successWithEvents user [FoundUser]
}
Caling getUserById 42 would return a successfully completed operation with the user and the event FoundUser. Calling getUserById for any other number (e.g. 0) would return a failed operation with the error event UserIdDoesNotExist 0. You would add more events as necessary.
Result<'a>type in F# 4.1. You are not getting the default value foroption<'a>, but the value for an empty list, which isnull, which shows up as None in that case. BasicallyErroris not a default value, it's just a constructor for the Error case. To get it you would need to pattern match or use the approach in the answer. Also, usingLinqin F# is possible but not idiomatic. - s952163try/withto handle them, especially since many will occur on the I/O side, and ROP which is great for DDD is not so great there. The option type then should handle the case when no user id exists, then pattern match on None. - s952163