[swift-evolution] typed throws

John McCall rjmccall at apple.com
Fri Aug 18 23:24:26 CDT 2017


> On Aug 18, 2017, at 11:43 PM, Mark Lilback <mark at lilback.com> wrote:
> 
>> 
>> On Aug 18, 2017, at 2:27 AM, John McCall via swift-evolution <swift-evolution at swift.org> wrote:
>> 
>> Even for non-public code.  The only practical merit of typed throws I have ever seen someone demonstrate is that it would let them use contextual lookup in a throw or catch.  People always say "I'll be able to exhaustively switch over my errors", and then I ask them to show me where they want to do that, and they show me something that just logs the error, which of course does not require typed throws.  Every.  Single.  Time.
> 
> We're doing it in the project I'm working on. Sure, there are some places where we just log the error. But the vast majority of the time, we're handling them. Maybe that's because we're using reactive programming and errors bubble to the top, so there's no need to write that many error handlers. And if I am just logging, either the error doesn't really matter or there is a TODO label reminding me to fix it at some point.

I'm not saying people only log errors instead of handling them in some more reasonable way.  I'm saying that logging functions are the only place I've ever seen someone switch over an entire error type.

I keep bringing exhaustive switches up because, as soon as you have a default case, it seems to me that you haven't really lost anything vs. starting from an opaque type like Error.

>> On Aug 18, 2017, at 3:11 PM, Matthew Johnson via swift-evolution <swift-evolution at swift.org> wrote:
>> 
>> The primary goal for me personally is to factor out and centralize code that categorizes an error, allowing catch sites to focus on implementing recovery instead of figuring out what went wrong.  Here’s some concrete sample code building on the example scenario above:
> 
> I'm using a similar approach. Here is some stripped down code:
> 
> //error object used throughout project
> public struct Rc2Error: LocalizedError, CustomStringConvertible, CustomDebugStringConvertible {
> 	/// basic categories of errors
> 	public enum Rc2ErrorType: String, Error {
> 		/// a requested object was not found
> 		case noSuchElement
> 		/// a requested operation is already in progress
> 		case alreadyInProgress
> 		/// problem parsing json, Freddy error is nested
> 		case invalidJson
> 		/// nestedError will be the NSError 
> 		case cocoa 
> 		/// nested error is related to the file system 
> 		case file 
> 		/// a wrapped error from a websocket 
> 		case websocket 
> 		/// a generic network error 
> 		case network 
> 		/// an invalid argument was passed (or parsed from json)
> 		/// wraps an unknown error
> 		case unknown
> 	}
> 
> 	/// the generic type of the error
> 	public let type: Rc2ErrorType
> 	/// the underlying error that caused the problem
> 	public let nestedError: Error?
> 	/// location in source code of where error happened
> 	public let location: String
> }

Okay.  I mean, this seems essentially like Swift's Error design.  The comment says you use this type ubiquitously in your project.  The type completely erases any differences between functions in terms of what errors they can throw.  Predictably, it includes an unknown case.  Code that processes values of this type must look essentially exactly like switching over an Error, except that the unknown case involves explicitly matching '.unknown' instead of using 'default'.

> //a domain-specific error type that will be nested
> public enum NetworkingError {
> 	case unauthorized
> 	case unsupportedFileType
> 	case timeout
> 	case connectionError(Error)
> 	case canceled
> 	case uploadFailed(Error)
> 	case invalidHttpStatusCode(HTTPURLResponse)
> 	case restError(code: Int, message: String)
> }

Okay.  So you're doing semantic tagging of errors — you're communicating out that an error arose during a specific operation.  And having some static enforcement here makes it easier to ensure that you've tagged all the errors.

> The most common errors don't need a nested error. The call site can figure out how to recover based on this. Using Result<T,E> I can specifically limit what kind of errors are possible from a function without using the wrapper error. E can always be specified as Error to ignore the typed system.

I see.  So you do have some functions that use a more specific type.

> It would be great if the native swift error system let you optionally put compiler-enforced constraints on what kind of error can be thrown. Then I can setup handlers for my specific type of error, but the compiler will give an error/warning if I'm not handling a possible Error. A generic catch-all is not proper error handling.

Do you really check for eight different cases at the call sites that get a NetworkingError, or is there a common predicate that you use?  Is there a reason that predicate cannot be written on Error?

> And if I'm calling something that doesn't throw type-constrained errors, I can use a generic handler and wrap it up in my own error. But the call site is getting details necessary to recover (if possible) without having to use the runtime to figure out what kind of error it is.

Can you talk about about why "without using the runtime" is an important requirement to you?  Why is an enum-switch with a catch-all case acceptable but a type-switch with a default unacceptable?

> I've got a very efficient system set up right now with great error handling. I don't see why the same capability can't exist in the language, especially when you can choose to completely ignore it. Hopefully more people would use it and we'd stop seeing so many "unknown error" dialogs.


John.


More information about the swift-evolution mailing list