I'll happily explain my requirements in more detail to show my "safety problems" with throws
:
-
I accept that
throw
ing functions in system libraries or 3rd party libraries I use in my app likeString.write(to:atomically:encoding:)
could throw any kind of error (although it would be really nice if common ones would be easier to explore so I can choose if I want to handle some). I could never potentially handle them all because I have no control over them and their release cycle is independent from my apps release cycle, so error cases could change anytime making it inconvenient to update my code. So if I call into these, I might catch a few cases I encounter during development (or through users reporting them) and handle them with my custom logic (e.g. presenting instructions to users how they can fix the issue they've encountered). The currentthrows
with catching the types I explicitly want to cover works fine here. Just documentation needs more improvement, I guess. -
I do not want to miss handling any of the errors I
throw
myself in my application, because I want to force myself to provide some useful custom logic to handle every error produced by my own code to make the user experience for my users as smooth as possible. This is really important to me, so I useenum
types in different parts of my application and add a new error case for every new assumption I have in code that is not being met by the data I find (mostly caller input data). By returning aResult
with my custom error enum I make sure the call side can easily switch over all possible error reasons and act accordingly (either by providing an automatic resolution or presenting the error with clear instructions and error highlighting to my users). -
When users come across any of my apps errors that could not be automatically resolved but are just presented with instructions to the users, I want my users to be able to report them to me as questions (so I can help them understand it better & maybe improve the instructions in the app) or as unexpected errors (so I can fix issues in my code when not the user did something weird but my app just had a bug). In any of these cases, in order to be able to investigate the issue, I don't just want to know the specific module the error was encountered, but I also want to know the entire "error call stack" so I know which screen the user was in, which other module this was calling, with other module that module was calling etc. until the error is encountered. To do that, I nest my error types, so an error type of a screen of my app can have 2 local error cases and 5 nested error cases. I define a custom protocol that all my error types need to adhere to so I can build that "error call stack" by concatenating the nested error cases unique identifier to the local error cases unique identifier, prepended by the entry error types unique type identifier, leading to an error code like "CFG-GPF" where "CFG" stands for the error type (so I know which top level screen/module the user was presented with the error) and the "GPF" is a list of nested error cases, e.g. "G: generateEnumFailed(StringsEnumError) => P: parsingError(ParsingError) => F: failed(expectedInput: String, remainingInput: Substring)".
Here's a typical error type in my app:
// `public` because my app is modularized using SwiftPM
public enum ConfigFileGeneratedCodeError: Error {
// MARK: - Leaf
case renameResourcesEnumFileFailed(errorDescription: String)
case cantOpenNonExistentEnumFile(expectedFileUrl: URL)
// MARK: - Nested
case writeTempFileFailed(error: ReadWriteFileError)
case appSandboxAccessError(error: AppSandboxAccessError)
case generateEnumFailed(error: StringsEnumError)
}
If I used throws
instead of Result
in all my APIs, on the throwing end I would need to:
- Ensure I don't miss documenting my error type for each function.
- Ensure I don't miss wrapping any 3rd-party errors into my custom error types.
With throws
instead of Result
, on the call site I would need to:
- Look up which error type is documented for each throwing function I call.
- Provide a
catch-all
for errors I forgot to wrap into my custom error type (see #2 above).
The throws
code would then look like this (note that I'm returning an Effect
in TCA):
// func start() -> throws
let baseDirUrl: URL
do {
baseDirUrl = try baseDirAccess.start()
} catch let unwrappedError as AppSandboxAccessError {
return .init(value: .errorOccurred(error: .appSandboxAccessError(error: unwrappedError)))
} catch {
return .init(value: .errorOccurred(error: .unexpectedError(error: error)))
}
Currently, by using Result
I don't have all the error-prone burden from above on the developer and my code looks like this instead:
// func start() -> Result<Void, AppSandboxAccessError>
let baseDirUrlResult = baseDirAccess.start()
guard let baseDirUrl = baseDirUrlResult.successValue else {
return .init(value: .errorOccurred(error: .appSandboxAccessError(baseDirUrlResult.failureError!)))
}
With my proposal, it would look like this and fix also the extra baseDirUrlResult
variable and the force-unwrapping (!
):
// func start() -> Result<Void, AppSandboxAccessError>
guard let baseDirUrl = baseDirAccess.start() catch {
return .init(value: .errorOccurred(error: .appSandboxAccessError(error)))
}
I hope that was somewhat clear. I'm open to elaborate more if you have more detailed questions.