According to the ocaml/v2.ocaml.org git log, the original version was written by Simon Cruanes around 2016. This is a fairly substantial update. The main contribution details how to use binding operators with option and result. Some more details are added to several other parts.
I’d love to have feedback here or directly in the GitHub PR.
I like the effort here, which is much needed and will be very valuable. I think there are two separate overarching issues here, which are currently interwoven and that it would be useful to disentangle them: error handling versus error generation.
One concern is that there are the three different forms of errors that one may encounter. It’s useful to explain what they are and how to deal with them.
A separate-but-related concern is deciding which types of errors to generate for different situations. Here, it’s important to step through the advantages and disadvantages of each and discuss the contexts in which one might choose one versus another. (And please don’t say that “exceptions should be used for exceptional situations.” Maybe it’s just me but I’ve never found that explanation to be helpful ) Much of this discussion isn’t OCaml-specific, so pointing to other recommended resources could be valuable and avoid reinventing the wheel.
It seems like a separate section could contain all of the information on converting among the different error forms, which isn’t difficult but often needs to be done.
Finally, perhaps there could be a separate section on where best to handle different types of errors and demonstrating techniques of error propagation/threading errors through one’s code. That might be too much for this page and, again, might be best handled by pointing to some external resources.
Good with an update to the tutorial. As it has such a broad coverage of different types of errors, I think covering using polymorphic variants as errors is pretty essential, as this pattern is used so often.
Edit: Specifically I’m thinking of certain concepts like:
opening closed polymorphic variants to include them in the same error-type
vs wrapping errors in another variant (e.g. if error-cases overlap)
matching on a subset of errors
alternating the program flow based on certain errors - e.g. contacting another server if the first one is offline
Thanks @rand, you are right; what you mention is missing. You accurately write: “This pattern is used so often”. I was thinking of updating the “Data Types and Matching” tutorial or writing an additional one on datatypes “beyond records and variants.” It would address polymorphic variants, GADTs and extensible variants. However, I agree some stuff needs to be added to the error-handling tutorial on those matters.
The situation for Stack_overflow has evolved, I think now it is reliable as far as OCaml code is concerned (but not C code).
The Out_of_memory exception exists but is not reliable.
One should be aware of the existence of asynchronous exceptions (e.g. Sys.catch_break), though how to properly handle them is beyond the scope of your introduction.
How to clean-up in case of an exception (the Stdlib now has Fun.protect)
Insights into choosing the right tool between exceptions and error types, e.g. “Having every operation marked as one that might fail is no more explicit than having none of them marked.”. (Though it contains the exact sentence “use exceptions for exceptional conditions” which is indeed terrible as far as helping users goes.)
There is a better alternative to the “use exceptions for exceptional conditions” saying; it has to do with how you program (implement & reason about) how the program recovers from the error. Essentially there is a way to program with exceptions such that immediate callers do not need to reason about exceptions that may arise (giving some meaning to such a notion of “exceptional condition”).
I wouldn’t say this is the point of “use exceptions for exceptional conditions” which I personally don’t see as unhelpful.
The point of the sentence is to force you to reason about the nature of the errors you are dealing with and whether you think it’s useful to make the client aware at the call point that a given error might occur when the program runs.
A typical example is a function that opens a user specified file. There are great chances that the file might not exist, it’s not exceptional for this to happen and clients will have to do something about it (e.g. report a nice error message rather than an obscure stack trace). Do you want to let clients think it does not happen (exception) or rather force them to think about the problem (result type).
Do you want to let clients think it does not happen (exception) or rather force them to think about the problem (result type).
Modulo your emphasis on API/client (whereas mine is on who handles the error), this corresponds to what I am saying: it has to do with how one reasons about exceptions vs. error types, and especially how you are not supposed to constantly think about exceptions that may arise, if done properly.
That’s why I dislike your formulation. For me the point is not about who handles the error, the point is how do we force the programmers, by design, into making programs that are user-friendly.
I believe your specific case does not contradict my standpoint. API design, while critical, tends to favor Result types as a common denominator for a diverse user base. However, this simply moves the problem towards helping users discern when to escalate an error into an exception.
The value judgement about errors depends on context, even for a straightforward example like the “file not found” error. Raising an exception might be completely acceptable (say, in a quick script with a hardcoded path). Knowing how the different kinds of errors must be handled is essential in choosing the best tool for the job and it avoids assuming a notion of value judgements about errors.
The distinction between “quick scripts” and programs is totally dubious. Good programs are those who error gracefully and meaningfully on users and there no reason to treat “quick scripts” differently.
A good error strategy and language level discipline can nudge programmers into doing the right thing by default without having them think about the problem too much. No need to choose the best tool for the job or make any “value judgement” about errors.
Would you be willing to expound upon this a bit more? I am not a professional programmer and continue to struggle with the conceptual differences between exceptions and result types, and when to use each. It sounds like you have an insight that I haven’t come across before.
Results wrap a function return value or an error value. They respect the functional paradigm and locality of control flow.
Exceptions are a mechanism by which an error is thrown upward in the call stack (unwinding it) up to an exception handler. This non-local control flow behaviour breaks the purity of the program and can lead to surprises, especially since OCaml does not provide a static analysis of the exceptional path (unlike with Java’s managed exceptions), especially the absence of an appropriate handler somewhere in the upper frames of the call chain.
I’m not asking what exceptions and result-types are but, rather, when exceptions are or may be preferable. Without invoking the old saw of “use exceptions to signal exceptional situations,” dismissing them entirely or suggesting that they should only be used for rapid prototyping/one-offs/scripting.
In other words: When working in a language like OCaml, where you have both exceptions and result types available, when might it be preferable to throw an exception rather than wrap the error?
Ah, but you asked about “conceptual differences”. Anyway, my intuition on this is you want to use Result types by default, and reserve exception for performance sensitive code paths (like most imperative features).
Thanks for the update of the error handling tutorial. I personally like very much the idea to use the types 'a option and ('a, 'e) result for error handling. In your tutorial you have described the monadic operators map and bind which makes using such types easier.
I would like to see the monadic operators already defined in the standard library. Because I use these operators a lot in my library fmlib I have defined an own version of the modules Option and Result. In the documentation I have shown how readability is improved using the monadic operators in the form of let* bindings.
But the idea of monadic error operators goes much further. Monadic operators can be use in combinator parsing or to decode javascript values in functional web applications.
I think using exceptions locally is perfectly fine.
I was always said that exceptions in ocaml are fast, so I don’t have any bad feeling about using some.
Sometimes, I define
exception Found of int (* or some other type *)
or
exception Break
and use it only in the code just after.
I think the Result type is more for if you have a pipeline, and you want this data processing pipeline to never crash.
You also don’t want to drop errors, but want them to propagate as is through the whole pipeline (they are probably logged somewhere at the end), while other values progress into the processing pipeline
and are being transformed along the way.
when might it be preferable to throw an exception rather than wrap the error?
I’m not sure there is a consensus on how to answer.
Using data-wrapped errors for pipelines or local recovery seems pretty consensual. But wrapping everything everywhere is overkill.
Spraying try-catch all over the code doesn’t seem reasonable, either. Robust applications must have a top-level exception handler.
To me, what makes OCaml great (as a language and as a community) is the gentle invitation to exercise thinking it carries. No silver bullet included; opinionated styles accepted; reasonings preferred over aphorisms. I’m trying to approach that topic in that spirit.
At some point in the future, this might stop to be true, due to the advent of hardware shadow stacks. This is not specific to OCaml, though; any longjmp-like code is becoming more and more contrived. For example, on x86, for security reasons, one cannot just set the shadow stack pointer to the old value when raising an exception; one needs to increment it in a loop until it reaches the value recorded when the exception handler was installed.
Would that also affect local exceptions? If so, that’d be very bad news
for OCaml. Local exceptions are just too useful in the imperative
fragment (and thus, for some performance-heavy domains) because OCaml’s
imperative constructs are just too weak otherwise in the absence of
break/return/continue/rust-like loop.
I certainly hope that exceptions remain fast for the foreseeable future.
I personally tend to use them a lot.