Skip to content

Instantly share code, notes, and snippets.

@nicklockwood
Last active April 8, 2024 14:26
Show Gist options
  • Star 93 You must be signed in to star a gist
  • Fork 15 You must be signed in to fork a gist
  • Save nicklockwood/c5f075dd9e714ad8524859c6bd46069f to your computer and use it in GitHub Desktop.
Save nicklockwood/c5f075dd9e714ad8524859c6bd46069f to your computer and use it in GitHub Desktop.
// Swift's untyped errors are a goddam PiTA. Here's the pattern I use to try to work around this.
// The goal is basically to try to guarantee that every throwing function in the app throws an
// ApplicationError instead of some unknown error type. We can't actually enforce this statically
// But by following this convention we can simplify error handling
enum ApplicationError: Error, CustomStringConvertible {
// These are application-specific errors that may need special treatment
case specificError1
case specificError2(SomeType)
...
// These are generic cases for errors that don't need special treatment
case message(String)
case generic(Error)
// Always handy to be able to print your errors
var description: String {
switch self {
case .specificError1, .specificError2:
// Application-specific
case let message(message):
return message
case let generic(error):
if let error = error as? CustomStringConvertible {
return error.description
}
// Always returns something, but not always something useful
return (error as NSError).localizedDescription
}
}
// Convenience constructor to save writing `ApplicationError.message(...)` all the time
init(_ message: String) {
self = .message(message)
}
// Convenience constructor for converting any unknown error to an ApplicationError
// this is useful when receiving errors where we're not sure what type they are,
// which is more common that not given Swift's lack of Error type annotations
init(_ error: Error) {
if let error = error as? ApplicationError {
self = error
} else {
self = .generic(error)
}
}
// By wrapping a call with this function, you can convert any thrown error to an ApplicationError
// usage 1: `let result = try ApplicationError.wrap(someFunctionWithNoArguments)`
// usage 2: `let result = try ApplicationError.wrap { try someFunction(with: arguments) }`
static func wrap<T>(_ closure: () throws -> T) throws -> T {
do {
return try closure()
} catch {
throw self.init(error)
}
}
// Like `wrap` above, but instead of calling the function and wrapping the error immediately,
// this returns a new function that throws an ApplicationError instead of the original error
// usage: let appErrorFn = ApplicationError.wrap(someUntypedErrorFn)
static func wrap<T>(_ closure: @escaping () throws -> T) -> () throws -> T {
return { try wrap(closure) }
}
// This function is basically an alternative version of try? that logs (or performs some other
// application-specific action) instead of failing silently. This is useful if you need to call
// a throwing function inside a function that doesn't throw, such as a delegate method
static func attempt<T>(_ closure: () throws -> T) -> T? {
do {
return try closure()
} catch {
let error = ApplicationError(error)
print(error.description) // Could do something more sophisticated, like store error in a global
return nil
}
}
}
@vermont42
Copy link

Thanks for sharing! Your approach inspired me as I created and used a new Error conformer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment