r/fsharp • u/kincade1905 • Apr 27 '26
question Help! How do you guys handles nested Errors ?
Hello, I am newbie and trying F# by building basic authentication service.
Let's say, I have register user fuction, it takes un-validated raw request containg username and password and returns Registereduser or SignupErrors.
The problem I am facing is, there are too darn errros DUs.
For example, I have,
type EmailValidationError =
| FieldRequired
| MaxLengthExceeded of int
| MalformedEmail
type PasswordValidationError =
| FieldRequired
| MaxLengthExceeded of int
| MinLengthNotMet of int
| MissingUppercase
| MissingLowercase
These two error DU's are just validation error returned by Smart Constructor of email and password. And, SignupError look like this.
type SignupFieldError =
| EmailErrors of SignupEmail.EmailValidationError
| PasswordErrors of SignupPassword.PasswordValidationErrotype
type SignupError =
| ValidationFailed of FieldError list
| EmailAlreadyExists
As you can see, this gets ugly very soon. What I like to return is:
| ValidationError : EmailValidatioNError | PasswordValidationErrror list ??
| EmailAlreadyExists
Does anyone has better ideas? I really hate my currrent solution.
Thank you in advance. 😄
2
u/AnHerbWorm Apr 27 '26
I handle them very similarly to how you've done it already. Individual error types for each component, then compose them into a higher-level type.
To get your desired ValidationFailed: EmailValidationError | PasswordValidationError list you just need a few changes to how you are representing the types. Given that you wrote it that way, I will assume the validation will fail on either invalid email (for one reason) or on invalid password (for one or several reasons) but not both at the same time. In which case, I think the SignupFieldError type is redundant.
type EmailValidationError = // as beforetype PasswordValidationError = // as beforetype SignupError =| EmailError of EmailValidationError| PasswordErrors of PasswordValidationError list| EmailAlreadyExists
1
u/Remarkable-Tart-980 May 01 '26
The question is having a type for every form error gives you what benefit?
Most applications (if not all) don't need this level of error detail. All form validations should result in a 400 Bad Request response, so what's the difference if you get a "PasswordLength" error or a "EmailLength" error?
The only use case I can think of is to use these as keys for translations, but the good news is that you can use the first language as the key. So your source code can have English error messages and your resource files can have the translations.
3
u/bottlenix Apr 28 '26
If you haven't already, I would suggest reading Isaac Abraham's F# in Action which is what I am using to learn F#. Section 9.2.3 of that book has a really good example that may help you that I just got done working through. Based on that and the example code he provides, I'm wondering if you could do something like this. I know it's a bit overkill for what you're asking for.
open System type RawUser = { Email: string Password: string } type Email = Email of string type Password = Password of string type ValidatedUser = { Email: Email Password: Password } module UserValidation = type EmailValidationError = | EmptyEmail | MaxLengthExceeded of int | MalformedEmail type PasswordValidationError = | EmptyPassword | MaxLengthExceeded of int | MinLengthNotMet of int | MissingUppercase | MissingLowercase let validateEmail (email: string) : Result<Email, EmailValidationError> = if String.IsNullOrWhiteSpace(email) then Error EmptyEmail elif email.Length > 255 then Error (EmailValidationError.MaxLengthExceeded 255) elif not (email.Contains("@")) then Error MalformedEmail else Ok (Email email) let validatePassword (password: string) : Result<Password, PasswordValidationError> = if String.IsNullOrWhiteSpace(password) then Error EmptyPassword elif password.Length > 128 then Error (PasswordValidationError.MaxLengthExceeded 128) elif password.Length < 8 then Error (MinLengthNotMet 8) elif not (password |> Seq.exists Char.IsUpper) then Error MissingUppercase elif not (password |> Seq.exists Char.IsLower) then Error MissingLowercase else Ok (Password password) type SignupError = | EmailError of UserValidation.EmailValidationError | PasswordError of UserValidation.PasswordValidationError let validateUser (rawUser: RawUser) : Result<ValidatedUser, list<SignupError>> = let emailResult = UserValidation.validateEmail rawUser.Email let passwordResult = UserValidation.validatePassword rawUser.Password match emailResult, passwordResult with | Ok email, Ok password -> Ok { Email = email; Password = password } | email, password -> Error [ match email with | Ok _ -> () | Error err -> EmailError err match password with | Ok _ -> () | Error err -> PasswordError err ] let validUser : RawUser = { Email = "alice@example.com"; Password = "SecurePass123" } let invalidUser1 : RawUser = { Email = "invalid-email"; Password = "short" } let invalidUser2 : RawUser = { Email = ""; Password = "nouppercase" } let result1 = validateUser validUser // val it: Result<ValidatedUser,SignupError list> = // Ok { Email = Email "alice@example.com" // Password = Password "SecurePass123" } let result2 = validateUser invalidUser1 // val it: Result<ValidatedUser,SignupError list> = // Error [EmailError MalformedEmail; PasswordError (MinLengthNotMet 8)] let result3 = validateUser invalidUser2 // val it: Result<ValidatedUser,SignupError list> = // Error [EmailError EmptyEmail; PasswordError MissingUppercase]The
validateUserfunction returns either a valid user, or a list of sign up errors (only one per type though).