As long as the OCaml compiler generates code that calls caml_raise_exception
for local exception, yes. That said, it will presumably take some time before shadow stacks end up getting enabled in OCaml programs. (I guess it will mostly depend on how long it takes before operating systems start rejecting programs that disable shadow stacks.) So, by that time, the compiler might have been taught how to transform the raise of local exceptions into simple jumps.
One tradeoff that I don’t see mentioned is that by using the Result monad you very likely lose the origin of the error, which can make tracking down the problem more difficult. Unless you use something like ‘reword_error’ appropriately often it is very easy to forget about this and just let the Result monad bubble the error up the callstack, losing all information on how the problem was reached, e.g. you won’t even know which function returned the error in the result if it is a generic error like failing to open a file,
and you won’t even know which file you failed to open.
Don’t get me wrong: I do like the use of 'result’s from an API design point of view, but it does require some extra thought and does have some pitfalls that are not apparent initially. Once you are aware of them you can make a better program using results.
Exceptions have a stacktrace by default, which is invaluable for debugging problems and knowing exactly which piece of code raised the problem. With results you only know what the error was, but not necessarily where in the code it was raised or the conditions under which it was reached.
It is worth thinking about including enough information in the error (whether results or exceptions are used) to make debugging possible. E.g. don’t just raise Not_found
, it is one of the most difficult exceptions to debug, a much better approach is to also include the key that you were looking for into the error result or exception.
Similarly if you failed to open a file say which file you failed to open.
In some situations logging can nicely complement this, especially if using some of the builtin locations like __LOC__
, __POS__
, or __LOC_OF__
, __POS_OF__
, or some other means of making log messages unique such that searching for them yields the position in the source code.
I think the basic principle behind all this could be “think about including enough information in your errors or exceptions to make reproducing/debugging the problem possible from the error/exception alone”. (otherwise you get a bugreport with an error and you may not even know how to start reproducing it if it is a generic `not found’ type error, and the error is sufficiently rare that it only happens e.g. once a month).
Thank you very much, @edwin; this is indeed very true. That’s yet another excellent piece of feedback I must add.
EDIT: oops, sorry @edwin. I replied to you specifically but my comment is targeted to all participants of this thread of course
Nice read
Mixing exceptions with Result/Option sounds undesirable to me, generally.
I feel like this could lead you to have the worse of both worlds: an effort has been put into representing errors as strict types, but your program could still break arbitrarily, leading to uncertainty and wasted effort.
Is there a way to enforce a particular style, let’s say at the module level?
Also, since you mentioned the performance implications of using Result/Option over exceptions, I’d like to understand more about it. Considering premature optimization, how would one identify a performance bottleneck caused by wrapper types?
I’m referring to this paragraph btw:
However, exceptions have the great merit of being compiled into efficient machine code.
When implementing trial and error approaches likely to back-track often, exceptions can be used to achieve good performance.
I don’t think this paragraph should belong in a tutorial for “Error handling”. It’s confusing. These “trial and error approaches” are not really about errors as implied by the general tutorial. It’s about using exceptions for control flow in the context of backtracking algorithms.
The scheme is usually that you have a point at which you start exploring a branch of some structure, when you get to a point where you realise the branch is not the one you are seeking for, you just raise an exception to get back at the branching point. That’s faster than returning from all the functions that lead you to point where you decided to abort the search.
Oh I see I mostly misread, thanks for the feedback!
I don’t think this paragraph should belong in a tutorial for “Error handling”. It’s confusing.
Thanks, @dbuenzli, you are right this is moving from handling exceptions to programming with exceptions which, although related, is a topic on its own.
I must admit I wasn’t very well inspired when picking this example to illustrate the previous point. Can you imagine something better?
Not sure. I don’t have definitive answers on all this. But maybe there’s something to say about the granularity of operations.
For example in the xmlm
streaming decoding API I don’t think I would return result values on each lexeme input. However I would definitively use one for the function that uses the API to decode a whole given XML format into an OCaml value.
In fact it’s not uncommon for me to use exceptions internally which I transform to an error result at the API boundary.
let public_fun … =
try …
with Failure msg -> Error msg