I’m not talking about crashing hard, you can fortunately catch exceptions and list and print out the sane error messages exactly at the same place as if you had used result:
switch processRequest() {
| Error(UnrecognizedArg({argName})) =>
Console.error(`Unrecognized argument: '${argName}'`)
Process.exit(2)
| Error(TooManyOccurrences({argName})) =>
Console.error(`Argument '${argName}' must only be specified once`)
Process.exit(3)
| Ok(result) => Console.log(`success: \n${result}`)
}
vs
switch processRequest() {
| exception CliExn(error) =>
switch error {
| UnrecognizedArg({argName}) =>
Console.error(`Unrecognized argument: '${argName}'`)
exit(2)
| TooManyOccurrences({argName}) =>
Console.error(`Argument '${argName}' must only be specified once`)
exit(3)
}
| result => Console.log(`success: \n${result}`)
}
The distinction does not come from the lack of monadic features but from the semantics and intended usage: will I process the error on the boundary of my program only or will I reuse the error somewhere else in my program flow?
If you use the same type for different usage, you’re likely going to misuse it, typically in this case by ignoring error cases that should have been checked before the boundary if you take the habit of unwrapping every result at the beginning of each function.
But sometimes, even though you know you need the error cases in different places of your program, there are functions where you only care about the ok case and there unwrapping results shines, I totally agree.