Skip to content

Instantly share code, notes, and snippets.

@Jeehut
Created June 26, 2022 15:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Jeehut/8be452f0a8e87f131538736067e5b409 to your computer and use it in GitHub Desktop.
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.
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