That is, a `throw` statement in Swift simply returns an `Error` value to the caller via a special return path instead of the normal result.
More explicitly, a Swift function declared as:
func f() throws -> T {
}
Could be read as func f() -> (T|any Error) {
}
More here: https://github.com/swiftlang/swift/blob/main/docs/ErrorHandl...Java experience taught us that, when writing an interface, it is common not to know the exception type. You often can’t know, for example, whether an implementation can time out (e.g. because it will make network calls) or will access a database (and thus can throw RollbackException). Consequently, when implementing an interface, it is common in Java to wrap exceptions in an exception of the type declared in the interface (https://wiki.c2.com/?ExceptionTunneling)
One could argue Rust is slightly better than Java, because in Rust there are no unchecked exceptions. However, in Rust there is panic, which is in a way like unchecked exceptions, which you can also catch (with panic unwinding). But at least in Rust, regular exceptions are fast.
But you get the same with checked exceptions in Java. Yes, an interface will say foo can only throw FooException, but if you want to do anything when you get a FooException, you have to look inside to figure out what exactly was wrong, and what’s inside that FooException isn’t limited.
A later version of the library may suddenly throw a FooException with a BarException inside it.
So as a user you could check for a generic file_not_found error, and if the underlying library uses http it could just pass on the 404 error_code with an http_category say, and your comparison would return true.
This allows you to handle very specific errors yet also allow users to handle errors in a more generic fashion in most cases.
[1]: https://www.boost.org/doc/libs/latest/libs/system/doc/html/s...
https://github.com/swiftlang/swift-evolution/blob/main/propo...
I say limited because the compiler doesn't (yet, as of 6.2) perform typed throw inference for closures (a closure that throws is inferred to throw `any Error`). I have personally found this sufficiently limiting that I've given up using typed throws in the few places I want to, for now.
That’s a huge limitation when writing libraries. If you have an old function that declares that it can throw a DatabaseError, you can’t e.g. add caching to it. Adding CacheError to the list of throwable types is an API breaking change, just like changing a return type.
Swift has typed errors now, but they shouldn’t be used carefully, and probably not be the default to reach for
1a. yes, there was some error 1b. there was an error--throw another local error and encapsulate the caught error 2. treat result of throwing call as `nil` and handle appropriately
I don't think typed throws add anything to the language. I think they will result in people wasting time pondering error types and building large error handling machines :)
When I used Java, I found typed exceptions difficult to reason about and handle correctly.
> if err != nil return err
When you call code that can throw (return an error via the special return path) you either have to handle it or make the enclosing context also throwing.
Assuming `canThrow()`, a function that might throw an `Error` type:
func canThrow() throws {
...
}
Call canThrow(), don't handle errors, just rethrow them func mightThrow() throws {
try canThrow() // errors thrown from here will be thrown out of `mightThrow()`
...
}
Alternatively, catch the errors and handle them as you wish: func mightThrow() throws {
do {
try canThrow()
} catch {
...handle error here
...or `throw` another Error type of your choosing
}
...
}
There are a few more ways to handle throwing calls.. For example- `try?` (ignore error result, pretend result was `nil`)
- `try!` (fail fatally on error result)
It's a shame. Were I designing a "low-level" or "systems" language, rather than put the cart before the horse and pick error results or exceptions, my litmus test would be how to make handling OOM as easy as possible. Any language construct that can make gracefully handling OOM convenient is almost by definition the fabled One True Way. And if you don't have a solution for OOM from the very beginning (whether designing a language or a project), it's just never gonna happen in a satisfactory way.
Lua does it--exceptions at the language level, and either setjmp/longjmp or C++ exceptions in the VM implementation. But for a strongly typed, statically compiled language, I'm not sure which strategy (or hybrid strategy) would be best.
Depending on the OS you likely won't even get the chance to handle the error because the allocation never fails, instead you just over commit and kill the process when physical memory gets exhausted.
I guess what I'm trying to say is designing your language's error handling primitives around OOM is probably not a good idea, even in a systems programming language, because it's a very rare class of generally unrecoverable error that only very carefully designed code can recover from.
Anyhow allocating isn't that absurd either. It keeps the size of Result<T, E> down by limiting the 'E' to be a pointer. Smaller Result<T, E> can help the compiler generate better code for passing the return value, and the expectation is the error path is rare so the cost of allocating might be outweighed by the lighter Result<T, E> benefits. Exceptions take a similar standpoint, throwing is very slow but the non-exceptional path can (theoretically) run without any performance cost.
I want to say the main driver for those right now is Rust for Linux since the "normal" panicking behavior is generally undesirable there.
It's funny how often functional programming languages lead the way, but imperative languages end up with the credit.
Java's checked exception is just an (very anti-ergonomic) implementation of tagged union type.
I think FP receives a lot of "credit."
Albeit the "pure" FP languages aren't popular, because 99% FP is really hard.
This is why I think checked exceptions are a dreadful idea; that, and the misguided idea that you should catch exceptions.
Only code close to the exception, where it can see through the abstraction, and code far away from the exception, like a dispatch loop or request handler, where the failure mode is largely irrelevant beyond 4xx vs 5xx, should catch exceptions.
Annotating all the exceptions on the call graph in between is not only pointless, it breaks encapsulation.
Just include the file name in the error message. And all of this is predicated on logging errors, which is not at all user-friendly, and not remotely acceptable in GUI applications.
That means that the top level handler can at best log a vague message.
That in turn means you must log along the way where you have more precise information about the failure, or you risk not having enough information to fix issues.
And that in turn means you must have lots of redundant logging, since each point in the stack doesn't know whether the abstraction it's invoking has already logged or not, or encapsulation would be violated.
Unable to create the media database. Failed to upgrade the database from version 3 to version 6. SQL Query failed. Syntax error at column 29 near "wehre". ("SELECT id`Thumbnail from tThumbailIndex wehre idFile=?").
(Messages are pre-pended to inner errors as one progresses up the stack). Yes, a gruesome error message. But it's a gruesome error condition.
I've also used inner exceptions in a large Enterprise product, and was very pleased with the result. (Java has them, .net has them, C++ and Typescript can have them if you want them). At the top level, the error message goes:
Unable to connect to the media database.
Failed to upgrade from version 3 to version 6.
SQL Query error failed.
Syntax error at column 6 near 'ndex.
("SELECT idI'ndex from T_ThumbnailIndex")
(each successive error message contained in a successive inner exception).
The advantage: no need to assemble multiple lines of logged errors from a logfile that end-users really don't want to be digging around in.A more prosaic and actually end-user-helpful chained message might be:
Unable to connect to the media database.
Failed to create the file /var/MediaServer/mediaIndex.db
Out of disk space.Yes, that's the idea. You split the information into a diagnostic with stuff you deal with now and data that the upper layer will handle. The intersection between the data in these two things should be empty.
> And that in turn means you must have lots of redundant logging, since each point in the stack doesn't know whether the abstraction it's invoking has already logged or not, or encapsulation would be violated.
No. You print a diagnostic, about what exactly THIS layer was trying to do, you don't speculate what the upper layer was trying to do and try to log that. Every layer knows the lower layer has already logged the primary error, because an error object exists, and it also knows that the upper layer will print a diagnostic about what was the intention, so it only prints exactly what the error was in this layer.
> doesn't know whether the abstraction it's invoking has already logged or not, or encapsulation would be violated.
It knows that the lower layer has logged all stuff that that layer considered to be important, and that none of the data that is available to this layer was logged, since that is the responsibility of the caller.
An example:
Document rendering incomplete, skipped publishing step. Thumbnail #39 missing. Failed to fetch image: Connection refused. [Discarded malformed packet with SYN flag. Invalid data in src/network/tcp.c:894 parse_tcp_packet_quirks_mode]
Depending on the log level, you wouldn't show the later diagnostics. If there is a debug flag set, you can also add the function/line information to every step, not just to the last. If you are outtputing to stuff like syslog, you would put each diagnostic on its own line.I think our disagreement is less about error handling and more about logging policy. I favour a logging policy which lets you understand what the code is doing even if it doesn't error out; this means that you don't need to log on unwind. You favour a logging policy specific for errors.
My position is that your position ends up with less useful context for diagnosing errors.
I use "unwinding" for diagnostics even in the happy case.
Awful stuff.
The filename to be opened is likely passed as a string in an argument, so it IS present in the function interface contract.
For example, it plays poorly with generics. (Especially if you start doing FP, lambdas.)
If Java added union types, it wouldn't be a big deal, but AFAIK this is still a limitation.
But they CAN be caught with a standard catch which may be non-intuitive if you don't know about them, and about that, in advance.