My mind is very hard wired towards using errors as value. I’m mainly a Go developer, I like Erlang and Elixir a lot, and I would like to use Rust more, but I think the language is just way too much. I have lots of worries with OCaml because of the lack of jobs, projects, community size, etc… but it really looks like to be a amazing language to learn and improve as a developer. But I don’t know if I’ll be able to adapt to the whole error handling and this is my question to other developers that came from languages that use errors as values like Go, Rust, and Erlang/Elixir. How do you feel about the overall development experience and error handling?
OCaml also supports non-exceptional representation of error-prone computations via
Option. See Error Handling · OCaml Tutorials
Erlang also has exceptions: Erlang -- Errors and Error Handling
IMO, OCaml’s “errors as values” story is far superior to go’s; and it is just as ergonomic, but less magical than Rust’s.
Rust also has effectual errors, but they are unrecoverable, even tho you’ll find widely used libraries with methods that panic on invalid inputs. That’s not nicer than a slick exception system, imo.
I’d be happy to help with specific questions on these points.
I have a similar disposition re: errors as values. Thankfully, almost four years in using OCaml quite heavily in a couple of different contexts, I’ve been able to largely not handle exceptions imperatively. The standard library (and common extensions like Jane Street’s and containers) now offers exception-free implementations for operations that would otherwise raise an exception (e.g.
find_opt returning an option vs.
Not_found), and most libraries IME likewise provide option and result returns, as appropriate. There are cases where one absolutely does need to be aware of the exception types that might be raised by a particular operation, but they’re rare enough to be the exception to the rule.
According to what I have seen, developers with a certain experience are able to sidestep a few minor rough edges here and there (eg in tooling) and quite enjoy the language and appreciate its rock-solid foundations, refactoring power, straight-forward compilation, speed, etc.
Error handling facilities are excellent: exceptions, error values, monads, algebraic effects, … OCaml does not enforce a single style of error handling: you can use whatever fits best in any given situation. This increases complexity somewhat (especially when using third-party libraries which use different error handling styles), but in practice you can produce robust code by following a few simple rules of thumb: error values for “errors” that do not represent an exception situation, exceptions for fatal errors or non-local control flow within library code (but not escaping through the public interface), etc.
Is there still a possibility that an exception will still occur for whatever reason, so we might as well add a single root try/catch to prevent the main process from shutting down?
Example: in nodejs or go, you better have that try/catch or rescue because a simple out-of-bounds exception can happen any time, and we wouldn’t want that crashing the main process.
That possibility is always there for any reasonably complex application.
Pick your favourite “not likely, but is happening somewhere right now” option:
- network failure
- disk failure
- file deleted/permissions changed by superuser
- memory “optimistically” granted by O.S. is now no longer available (this is how they work nowadays)
- user/superuser issued a kill command
- my all-time favourite memory bit flipped by cosmic ray (varies according to altitude)
So, even if “out of bounds” won’t bother you, and “divide by zero” will never happen in your code, if you want to do try and cope with the “real world” have something that catches errors, logs them then cleans up and restarts the process.
I’ve written two main types of programs in OCaml:
- Standalone programs that are meant to be executed for a tightly-defined task. In this context, almost any unexpected failure should rightly be left alone to panic the process. Something higher-level will cope, even if that something else is logged alerts, etc.
- Web apps, using e.g. Dream et al. These are generally running in an event loop that generally has catch-all exception handling baked in that gives me the opportunity to log the problem(s) and display a reasonable message to the user or perhaps take some alternative path. This doesn’t apply for really egregious cases like OS failures, OOM, etc., where panicking still remains the only available option.
Other contexts may require more special attention.
Right, just making food for thought.
Regarding exceptions, even Haskell has it. I haven’t dug too much into it but how does a system like Rust do away with nearly all exceptions? It seems improbable. They have panics but those are meant for unrecovery errors, meaning that your app would need a complete restart.
In the case of a bit flip, will the rust “runtime” be able to detect a cosmic bit flip that corrupts something entirely? And then be intelligent enough to panic?
I guess at some point, restarting is simply best.
Oh, one thing that is worth keeping in mind is that top-level definitions are all executed outside of what you think of as your “main” entry point, and each one could raise an exception, so there’s no “easy” way to really have a “single root try/catch”. So, for all such definitions, you need to do one of:
- Be very sure that the top-level expression won’t fail (again, aside from truly unrecoverable stuff like OOM).
- Define them to be lazy values, so that when they are accessed from e.g. inside an event loop, anything bad that happens can be caught and reported cleanly.
Alternatively, you could be very judicious in not having such top-level definitions. This is what I do for my most complex applications, where I have explicit lifecycle abstractions for initializing what would otherwise be top-level values, and then ways for the various parts of the app to access those values from some central context.
I think you have a misunderstanding of Rust panics. By default (which is called an unwind panic) they are recoverable and are used in exactly the same way as the unforeseen and unhandled exceptions being discussed. The web server (e.g., Hyper) will catch them and log them, so that other request handlers are not impacted, and you can do the same if you need to.
Rust panics can be configured to abort, meaning they don’t unwind and cannot be recovered from. But doing this is almost always a mistake.