by ameliaquining | 2025-07-30 04:53:23+00:00
So for baseline context, we can consider two kinds of errors; there's no standard terminology, but I'll call them "exceptional results" (caused by problems outside your process's control, like filesystem or network failures) and "runtime errors" (caused by bugs in your code, like trying to dereference a null pointer or index past the end of an array).

Pony: Doesn't distinguish between exceptional results and runtime errors; either a function is guaranteed not to fail, or it isn't. A function that isn't is called "partial", and can only be called either from another partial function or from a try block that specifies how to handle errors. So anything that can fail in any way, including indexing into an array, has to be partial; there's no way to tell the compiler "please don't force my caller to handle the possibility of a bounds check failure, I'm sure that won't happen, and if it does then everything is terminally messed up and we should just crash". Likewise with a value of a union type being None when it shouldn't be, or otherwise being the wrong type. (They tried having integer division be partial, but apparently that was a bridge too far—so instead, any integer divided by zero is zero.) The top-level error-handling pattern would need to be implemented by making basically every non-leaf function in your codebase partial. Also, these errors can't have payloads or anything, so the top-level handler can't make use of any diagnostic information (if you want that then you need to instead return a custom union type and forgo the partial-function syntactic sugar). I haven't actually used Pony in anger but this is definitely the feature of the language that impresses me least; it's ostensibly in the name of correctness, by prohibiting programs from crashing, but you can't actually prevent the root causes of crashes without dependent types, so instead they've made this mess that makes effective error handling impossible.

Rust: Exceptional results are represented with the Result type, instances of which are just values returned from a function like any other. There's syntactic sugar (the ? operator) to propagate an error up a stack frame. A Result has a payload whose type must be specified; ergo, every function's API must effectively declare not only whether it can return exceptional results, but also what kinds. In a library, you'd typically design a structured error type to represent the possible exceptional results that your API can return, but in a binary, you may not care about that level of detail in each function's API, and instead prefer to return a catchall "some exceptional failure, idk what" type. Out of the box the language has Box<dyn Error> for this, but it's more common to use a third-party crate like anyhow or eyre that makes this pattern more ergonomic. In an application that handles errors this way, most likely every function that does I/O returns Result<T, Box<dyn Error>> (or the anyhow or eyre equivalent), while functions that just do pure computation on trusted data return non-Result types. Runtime errors use a different system, panics, which are not part of a function's API (so any function can panic); by default, panicking unwinds the call stack until it reaches a catch_unwind call or the bottom frame of the current thread's stack (allowing a top-level handler to use catch_unwind or Thread::join to catch and handle runtime errors), but when building an executable binary you can instead configure it to skip all that and immediately crash the process when a panic occurs (there are pros and cons to doing this). For this reason, library code can't rely on catch_unwind working, so it's important for libraries not to try to use panics to convey recoverable errors. A Result can be turned into a panic with the unwrap method, if you're sure that the relevant exceptional result can't occur or if you're just being lazy/writing prototype throwaway code and don't want to bother getting the function signatures right.

Swift: Exceptional results work like checked exceptions in Java, but without all the problems that make the latter not work very well in practice. Each function signature can either never throw anything (the default), throw any Error (with an arbitrary payload whose type is unknown at compile time, analogous to Rust's Box<dyn Error>), or throw a specific Error subtype with a specified payload (this last one is uncommon). When calling a throwing function, you must either handle the error or (if you throw a compatible type) propagate it up to your own caller; there are various syntax-sugar niceties to handle this. There's a Result type like in Rust but it's not commonly used as a return value, it's mostly only needed if you're storing errors in data structures or similar. So a top-level error handler is done by having all functions that do I/O be throwing functions, like in Rust but with nicer syntax. Runtime errors are not declared in function signatures and always immediately crash the process; unlike in Rust, there is no way to have a top-level handler for them. Like in Rust, you can turn an exceptional result into a runtime error (Swift's syntax for this is the try! operator).