LocalizedError.errorDescription gets lost for wrapped errors - swift

While optimizing error handling in my Swift application, I encountered the following odd behavior. My custom error types implement LocalizedError so I can provide errorDescription to the user. Since I cannot rely on third-party libraries to do the same, I have an ErrorWrapper enum with only one case underlying(_: Error):
import Foundation
enum CustomError: Error {
case test(_: String)
}
extension CustomError: LocalizedError {
var errorDescription: String {
switch self {
case let .test(err):
return "An error occured: \(err)"
}
}
}
enum ErrorWrapper: Error {
case underlying(_: Error)
}
extension ErrorWrapper: LocalizedError {
var errorDescription: String {
switch self {
case let .underlying(error) where error is LocalizedError:
// I verified execution jumps into this block,
// but error.errorDescription is nil
return (error as! LocalizedError).errorDescription ?? "Unknown error"
case let .underlying(error):
return error.localizedDescription
}
}
}
let err = CustomError.test("Foo")
let wrapped = ErrorWrapper.underlying(err)
print(err.errorDescription) // prints: "An error occured: Foo\n"
print(wrapped.errorDescription) // prints: "Unknown error\n"
Is there a way to solve this problem? I thought about implementing my custom Error protocol, but would like to stick with Swift's builtin error protocols, if possible.

Related

Swift: How to cast error to generic struct

I am being passed an Error at runtime. This error is actually a generic struct. I need to cast the error to this struct so I can get its details. How can I do this?
Code Example:
protocol MinorErrorType: Error {}
struct MajorError<T: MinorErrorType>: Error {
let minorError: T
}
enum SomeMinorError: MinorErrorType {
case error
}
func getName(_ error: Error) -> String {
"Some Error"
}
func getName<T: MinorErrorType>(_ error: MajorError<T>) -> String {
"MajorError"
}
func printName(_ error: Error) {
print(getName(error))
}
let error = MajorError<SomeMinorError>(minorError: .error)
printName(error)
// output:
// Some Error
You can see in the above code the generic getName is not called. If there is a solution where I only need to modify func printName that would be awesome.
Update: In production, I want to use this pattern for logging. I want a logger to be passed an error and be able to log MajorError's. I do not want to have to cast to MajorError<SomeMinorError> in getName as that would mean I would need to cast for all new implementations of MinorErrorType. This would make my logger need to know about too much information.
In the meantime, I used type erasure (here is a great article on the subject)
protocol MajorErrorType: Error {
func eraseToAnyMajorError() -> AnyMajorError
}
enum AnyMajorError {
case majorError(MinorErrorType)
}
protocol MinorErrorType: Error {}
struct MajorError<T: MinorErrorType>: Error {
let minorError: T
func eraseToAnyMajorError() -> AnyMajorError {
.majorError(minorError)
}
}
...
func printName(_ error: Error) {
if let majorError = (error as? MajorErrorType)?.eraseToAnyMajorError() {
print(getName(majorError))
else {
print(getName(error))
}
}
This lets the rest of my code use the generic structs (fun) and my logger in the dark about other error types.
Not sure if it helps, but I would use proper encapsulation to help with this issue. I.e. define getName as a function of your struct, enum, etc., so you can call it on appropriate type:
protocol MinorErrorType: Error {}
extension Error {
func getName() -> String {
"Some Error in extension"
}
}
struct MajorError<T: MinorErrorType>: Error {
let minorError: T
func getName() -> String {
"MajorError in struct"
}
}
enum SomeMinorError: MinorErrorType {
case error
}
Now inside printError you can control what to call:
func printName(_ error: Error) {
if let majorError = error as? MajorError<SomeMinorError> {
print(majorError.getName())
} else {
print(error.getName())
}
}
So if you call
let error = MajorError<SomeMinorError>(minorError: .error)
printName(error) // prints MajorError in struct
But you don't need printName function with it's casting at all: you can call getName(), and it will call getName from Error extension only if nothing else is defined:
let error = MajorError<SomeMinorError>(minorError: .error)
let smthElse = NSError(domain: "s", code: 123, userInfo: nil)
print(error.getName()) // prints MajorError in struct as before
print(smthElse.getName()) // prints Some Error in extension
You just need to switch your error. No need to create a another method:
func getName(_ error: Error) -> String {
switch error {
case is MajorError<SomeMinorError>: return "MajorError"
default: return "Some Error"
}
}
To access your error properties you can cast the error instead of checking its type:
func getName(_ error: Error) -> String {
switch error {
case let error as MajorError<SomeMinorError>:
return "\(error.minorError)"
default: return "Some Error"
}
}
Playground testing:
protocol MinorErrorType: Error {}
struct MajorError<T: MinorErrorType>: Error {
let minorError: T
}
enum SomeMinorError: MinorErrorType {
case minor
}
func getName(_ error: Error) -> String {
switch error {
case let error as MajorError<SomeMinorError>:
return "\(error.minorError)"
default: return "Some Error"
}
}
let error = MajorError<SomeMinorError>(minorError: .minor)
print(getName(error)) // "minor\n"
You can also give your error a RawValue:
enum SomeMinorError: String, MinorErrorType {
case minor
}
func getName(_ error: Error) -> String {
switch error {
case let error as MajorError<SomeMinorError>:
return error.minorError.rawValue
default: return "Some Error"
}
}

Swift - is there a way to match an error without throwing?

In some cases, an error is provided as an argument rather than being thrown. One example could be completion handlers where Result<T, Error> is provided. I would like to match the error without throwing it.
Example of error matching by throwing it:
enum MyError: Error {
case error
}
func process(error: Error) {
do {
throw error
} catch MyError.error {
print("this is it")
} catch {
print("unknown error")
}
}
process(error: MyError.error)
UPDATE:
The matching should work even for the system provided errors, like URLError which are not enum types.
func process(error: Error) {
do {
throw error
} catch URLError.timedOut {
print("this is not it")
} catch URLError.cancelled {
print("this is it")
} catch {
print("unknown error")
}
}
process(error: URLError(.cancelled))
You can use the if case syntax.
In your example,
enum MyError: Error {
case error
}
func process(error: Error) {
if case MyError.error = error {
print("this is it")
} else {
print("unknown error")
}
}
process(error: MyError.error)
There are several ways to match errors that are not thrown. A way that keeps the code simple with many errors to check for, is the use of a switch statement:
Update:
I have now updated the code to include the URLError that you have requested.
enum CommonError: Error {
case input
case output
}
enum RareError: Error {
case language
case mathematics
}
let error: Error = RareError.language
// or
let error: Error = URLError(.cancelled)
switch error {
case CommonError.input: print("Common input error")
case CommonError.output: print("Common output error")
case RareError.language: print("Rare language error") // This is executed
case RareError.mathematics: print("Rare mathematics error")
case URLError.cancelled: print("URLError -> cancelled") // - or this
case URLError.timedOut: print("URLError -> timed out")
default: print("The error is unknown; maybe you should consider throwing it now?")
}

How to map one error to a different error with RxSwift

Is it possible to map the error type with RxSwift?
For example, I have a helper function like this:
enum InternalError: Error {
case fooError
case barError
}
private func helper() -> Observable<MyData> {
// ...
}
enum ServiceError: Error {
case fatalError(message: String)
case warning(message: String)
}
I am hoping that we can do sth like this, notice the following mapError doesn't exist:
func getData() -> Observable<MyData> {
helper().mapError({ error: InternalError -> ServiceError
switch error {
case .fooError:
return ServiceError.fatalError(message: "This is bad.")
case .barError:
return ServiceError.warning(message: "Be careful.")
}
})
}
Is it possible? Or is it a bad idea in general?
What you described in you problem is impossible to do by returning an error because the return type of your function is an Observable<MyData>. There are two solutions:
You could define a enum Result<T> with .success(T) and .error(Swift.Error) and return Observable<Result<MyData>> where you would handle your errors as follows:
helper().catchError{ error in
guard let error = error as? InternalError else { throw UnknownError() }
switch error {
case .fooError:
return Observable.just(
Result.error(
ServiceError.fatalError(message: "This is bad.")
)
)
case .barError:
return Observable.just(
Result.error(
ServiceError.warning(message: "Be careful.")
)
)
}
}
Or you could simply rethrow by substituting return Observable... with a throw which I personally think is better than a Result<T> overhead.

Generate Error Sheet from Swift Exception

I'd like to generate an NSAlert-style error sheet from a Swift exception handler. In the past with Objective-C I could simply use [NSAlert alertWithError:] Now with Swift and it's own exception handling system in the mix, alerts feel rather clunky:
enum Problems: ErrorType {
case Bad
case Worse
}
func f() throws {
throw Problems.Worse
}
func g() {
let errorMessage: String
do {
try f()
}
catch Problems.Bad {
errorMessage = "This was bad"
}
catch Problems.Worse {
errorMessage = "This was even worse"
}
catch {
errorMessage = "This was unexpected"
}
guard errorMessage == nil else {
let alert = NSAlert()
alert.messageText = "Error"
alert.informativeText = errorMessage
alert.beginSheetModalForWindow(self, completionHandler: { [unowned self] (returnCode) -> Void in
if returnCode == NSAlertFirstButtonReturn {
// Handle the response
}
})
}
This feels kludgy and like it doesn't scale well if there are many error conditions that must be checked. Is there a better way?
Swift allows your ErrorType to have associated data. This is more or less an exploded version of userInfo from NSError. If you want convenience, you can write a function to convert a Problems enum to NSError:
enum Problems: ErrorType {
case Small
case Bad(message: String?)
case Worse(message: String?)
func toNSError() -> NSError {
var errorMessage = ""
switch self {
case .Small:
errorMessage = "Small error"
case let .Bad(msg):
errorMessage = msg ?? "Default bad message"
case let .Worse(msg):
errorMessage = msg ?? "Default worse message"
}
return NSError(domain: NSCocoaErrorDomain, code: 1, userInfo: [NSLocalizedDescriptionKey: errorMessage])
}
}
func f() throws {
throw Problems.Worse(message: nil)
}
do {
try f()
} catch let error as Problems {
let err = error.toNSError()
// now display NSAlert with err...
}
You can throw an error with message: nil to get the default message, or add your own custom message from the local conditions:
throw Problems.Bad(message: "I have a bad feeling about this")

Catch multiple errorTypes?

I'm looking for a way to catch multiple types of errors in a catch. I've tried fallthrough and the comma separated style from a switch statement and neither works. The docs say nothing about catching multiple but pattern 1. It's not clear to me which of the pattern syntaxes would work here.
Error definitions (sample):
enum AppErrors {
case NotFound(objectType: String, id: Int)
case AlreadyUsed
}
Works:
do {
//...
} catch AppErrors.NotFound {
makeNewOne()
} catch AppErrors.AlreadyUsed {
makeNewOne()
} catch {
print("Unhandled error: \(error)")
}
Does not compile, is it possible to do something like this?
do {
//...
} catch AppErrors.NotFound, AppErrors.AlreadyUsed {
makeNewOne()
} catch {
print("Unhandled error: \(error)")
}
If you want to catch all AppErrors, you can use this pattern:
catch is AppErrors
If you're looking for more specific matching, it seems to quickly get ugly.
This will let us catch specific cases of AppErrors:
catch let error as AppErrors where error == .NotFound || error == .AlreadyUsed
There's also this syntax which seems to work:
catch let error as AppErrors where [.NotFound, .AlreadyUsed].contains(error)
For completeness sake, I'll also add this option, which allows us to catch errors of two different types, but it doesn't allow us to specify which case within those types:
catch let error where error is AppErrors || error is NSError
Finally, based on the fact that anything we catch will conform to the ErrorType protocol, we can clean up the second & third examples I provided with an ErrorType extension and use that in conjunction where a where clause in our catch:
extension ErrorType {
var isFooError: Bool {
guard let err = self as? AppErrors else { return false }
return err == .NotFound || err == .AlreadyUsed
}
}
And just catch it like this:
catch let error where error.isFooError
You can create a case that contains an AppErrors array:
indirect enum AppErrors: Error {
case NotFound
case AlreadyUsed
case errors([AppErrors])
}
Then, for the catch statement:
catch AppErrors.errors(let errors) where errors == [.NotFound, .AlreadyUsed]
Do note, however, that errors is an Array and the order matters when comparing with ==. An alternative is to use case errors(Set<AppErrors>), but that would require AppErrors to conform to Hashable protocol.
UPDATE: Come to think of it, it's better to use an OptionSet type:
public struct InvalidInput: OptionSet, Error {
public var rawValue: Int
public init(rawValue: Int) {
self.rawValue = rawValue
}
public static let noAccount = InvalidInput(rawValue: 1 << 0)
public static let noKey = InvalidInput(rawValue: 1 << 1)
public static let invalidKey = InvalidInput(rawValue: 1 << 2)
}
func validateInput() throws -> Void {
var invalid: InvalidInput = []
invalid.insert(.noKey)
invalid.insert(.noAccount)
if !invalid.isEmpty {
throw invalid
}
}
do {
try validateInput()
} catch [InvalidInput.noAccount, InvalidInput.noKey] as InvalidInput {
print("Account and key are both required.")
}
Link: http://www.chrisamanse.xyz/2016/12/03/catching-multiple-errors-in-swift
Multi-Pattern Catch Clauses proposal (SE-0276) is implemented in Swift 5.3
And now code snippet from the question is valid:
do {
//...
} catch AppErrors.NotFound, AppErrors.AlreadyUsed {
makeNewOne()
} catch {
print("Unhandled error: \(error)")
}
You can check updated docs.