-
-
Save Jeehut/8be452f0a8e87f131538736067e5b409 to your computer and use it in GitHub Desktop.
Convenience APIs for nested precise-typed error reporting in Swift as of 2022.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
// MARK: - HelpfulError Type | |
public protocol HelpfulError: Error, Identifiable, CustomStringConvertible where ID: CustomStringConvertible { | |
associatedtype TypeID: Identifiable & CustomStringConvertible | |
/// A global unique identifier of the error type. Use a short 2-4 character uppercase alpha-numerics in ASCII. | |
/// Used to identify the root error type in unique error codes. | |
/// - WARNING: Don't use this directly. Use a specific error instances ``errorCode`` instead, it prepends this ``typeID``. | |
static var typeId: TypeID { get } | |
/// A unique ID within the error types scope. Use the most fitting uppercase alpha-numerics in ASCII. Combine nested IDs by appending at end. | |
/// Used to create a unique error code together with ``typeId`` for easy web searching a nested error case (thus the ASCII recommendation). | |
/// - WARNING: Don't use this directly. Use ``errorCode`` instead, it prepends the ``typeId``, too which is important to uniquely identify an error. | |
var id: ID { get } | |
/// A localized message describing what error occurred. Use full sentences here. | |
/// - WARNING: Don't use this directly unless you're planning to use the ``errorCode`` directly, too. Use ``errorMessage`` instead, it appends the error code to uniquely identify the error. | |
var errorDescription: String { get } | |
} | |
extension HelpfulError { | |
/// A unique error code useful for searching or reporting this specific issue in an issue tracker. | |
public var errorCode: String { | |
"\(Self.typeId.id)-\(self.id)" | |
} | |
/// A human-facing representation of the ``errorDescription`` including the ``errorCode`` for simple ``String`` error reporting. | |
public var errorMessage: String { | |
"\(self.errorDescription) (Error Code: \(self.errorCode))" | |
} | |
} | |
extension HelpfulError { | |
public var description: String { | |
self.errorMessage | |
} | |
public var localizedDescription: String { | |
self.errorMessage | |
} | |
} | |
// MARK: - Result Extensions | |
extension Result where Success == Void { | |
/// Convenience function to use in place of `.success(())` when returning success case for `Void` value (gets rid of the `()` parameter). | |
public static func success() -> Self { | |
.success(()) | |
} | |
} | |
extension Result { | |
/// Convenience access to the `.success` parameter if the ``Result`` is a success, else `nil`. If this is `nil`, ``failureError`` is guaranteed to be not `nil`. | |
/// | |
/// Thanks to this and ``failureError``, instead of a switch like this: | |
/// ``` | |
/// let value: SomeResponse | |
/// switch networkCall() { | |
/// case .success(let success): | |
/// value = success | |
/// | |
/// case .failure(let error): | |
/// return .failure(error) | |
/// } | |
/// | |
/// // some code using `value` | |
/// ``` | |
/// | |
/// You can write shorter code like that: | |
/// ``` | |
/// let result = networkCall() | |
/// guard let value = result.successValue else { return .failure(result.unwrappedError) } | |
/// | |
/// // some code using `value` | |
/// ``` | |
public var successValue: Success? { | |
switch self { | |
case .success(let value): | |
return value | |
case .failure: | |
return nil | |
} | |
} | |
/// Convenience access to the `.failure` parameter if the ``Result`` is a failure, else `nil`. If this is `nil`, ``successValue`` is guaranteed to be not `nil`. | |
/// | |
/// Thanks to this and ``successValue``, instead of a switch like this: | |
/// ``` | |
/// let value: SomeResponse | |
/// switch networkCall() { | |
/// case .success(let success): | |
/// value = success | |
/// | |
/// case .failure(let error): | |
/// return .failure(error) | |
/// } | |
/// | |
/// // some code using `value` | |
/// ``` | |
/// | |
/// You can write shorter code like that: | |
/// ``` | |
/// let result = networkCall() | |
/// guard let value = result.successValue else { return .failure(result.unwrappedError) } | |
/// | |
/// // some code using `value` | |
/// ``` | |
public var failureError: Failure? { | |
switch self { | |
case .success: | |
return nil | |
case .failure(let error): | |
return error | |
} | |
} | |
} | |
// MARK: - Custom Error Usage Example | |
enum MyErrorType: String, Identifiable, CustomStringConvertible { | |
case apiError = "API" | |
case screenError = "SCR" | |
var id: String { self.rawValue } | |
var description: String { self.rawValue } | |
} | |
enum ApiError: Error { | |
case bodyDecodingError(error: Error) | |
case badRequest(message: String) | |
case internalServerError | |
case requestTimeOut | |
} | |
extension ApiError: HelpfulError { | |
static let typeId: MyErrorType = .apiError | |
var id: String { | |
switch self { | |
case .bodyDecodingError: | |
return "D" | |
case .badRequest: | |
return "B" | |
case .internalServerError: | |
return "I" | |
case .requestTimeOut: | |
return "T" | |
} | |
} | |
var errorDescription: String { | |
switch self { | |
case .bodyDecodingError(let error): | |
return "Failed decoding body: \(error.localizedDescription)" | |
case .badRequest(let message): | |
return "Invalid request: \(message)" | |
case .internalServerError: | |
return "An internal server error ocurred. Please try again later or report a bug." | |
case .requestTimeOut: | |
return "Request timed out. Please check your internet connection." | |
} | |
} | |
} | |
// MARK: - Custom Error Reporting Example | |
struct ProfileResponse: Decodable { | |
let name: String | |
let imageUrl: URL | |
} | |
func fetchProfile() -> Result<ProfileResponse, ApiError> { | |
enum FakeNetworkResponse { | |
case success(jsonBody: String) | |
case badRequest(message: String) | |
case internalServerError | |
static func randomCase() -> Self? { | |
let possibleCases: [Self?] = [ | |
Self.success(jsonBody: #"{ name: "Severus", imageUrl: "https://images.com/abc.png" }"#), | |
Self.success(jsonBody: "{}"), | |
Self.badRequest(message: "Title must not be empty."), | |
Self.internalServerError, | |
nil | |
] | |
return possibleCases.randomElement()! | |
} | |
} | |
guard let response = FakeNetworkResponse.randomCase() else { return .failure(.requestTimeOut) } | |
switch response { | |
case .success(let jsonBody): | |
do { | |
return .success(try JSONDecoder().decode(ProfileResponse.self, from: jsonBody.data(using: .utf8)!)) | |
} catch { | |
return .failure(.bodyDecodingError(error: error)) | |
} | |
case .badRequest(let message): | |
return .failure(.badRequest(message: message)) | |
case .internalServerError: | |
return .failure(.internalServerError) | |
} | |
} | |
// MARK: - Calling into a function with custom error reporting | |
enum ScreenError: Error { | |
case apiError(ApiError) | |
} | |
extension ScreenError: HelpfulError { | |
static let typeId: MyErrorType = .screenError | |
var id: String { | |
switch self { | |
case .apiError(let error): | |
return "A" + error.id | |
} | |
} | |
var errorDescription: String { | |
switch self { | |
case .apiError(let error): | |
return error.errorDescription | |
} | |
} | |
} | |
func loadScreenData() -> Result<Void, ScreenError> { | |
// ... | |
let profileResult = fetchProfile() | |
guard let profileResponse = fetchProfile().successValue else { | |
return .failure(.apiError(profileResult.failureError!)) // <-- currently I have to force-unwrap the `failureError` | |
} | |
print("Fetched profile of ", profileResponse.name) | |
// ... | |
return .success() | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment