Swift - Error propagation while maintaining api clarity - ios

With the addition of ErrorType to Swift, it's now possible to express error and failure events in a cleaner, more concise manner. We're no longer bound as iOS developers to the old way of NSError, clunky and hard to use.
ErrorType is great for a few reasons:
Generic parameters in functions
Any enum can conform to and implement it
Works better with the catch/throw paradigm
There are however some problems and one problem in particular that I've been running into lately and I'm curious to see how others solve this.
Say for example, you're build a social networking application akin to Facebook and you have Group model. When the user first loads your application you want to do two/three things:
Fetch the relevant groups from your server.
Fetch the already persisted groups on disk (Realm, CoreData, etc)
Update the local copy with the remote copy just fetched.
Throughout this process, you can break the types of errors up into two distinct categories: PersistenceError and NetworkError where the ErrorType conforming enums might look like
enum PersistenceError: ErrorType {
case CreateFailed([String: AnyObject)
case FetchFailed(NSPredicate?)
}
enum NetworkError: ErrorType {
case GetFailed(AnyObject.Type) // where AnyObject is your Group model class
}
There are several ways/design patterns you can use for delivering errors. The most common of course is try/catch.
func someFunc() throws {
throw .GetFailed(Group.self)
}
Here, because functions that throw can't yet specify what type of error they're throwing, although I suspect that will change, you can easily throw a NetworkError or a PersistenceError.
The trouble comes in when using a more generic or functional approach, such as ReactiveCocoa or Result.
func fetchGroupsFromRemote() -> SignalProducer<[Group], NetworkError> {
// fetching code here
}
func fetchGroupsFromLocal() -> SignalProducer<[Group], PersistenceError> {
// fetch from local
}
Then wrapping the two calls:
func fetch() -> SignalProducer<Group, ???> {
let remoteProducer = self.fetchGroupsFromRemote()
.flatMap(.Concat) ) { self.saveGroupsToLocal($0) // returns SignalProducer<[Group], PersistenceError> }
let localProducer = self.fetchGroupsFromLocal()
return SignalProducer(values: [localProducer, remoteProducer]).flatten(.Merge)
}
What error type goes in the spot marked ???? Is it NetworkError or PersistenceError? You can't use ErrorType because it can't be used as a concrete type and if you try using it as a generic constraint, <E: ErrorType>, the compiler will still complain saying it expects an argument list of type E.
So the issue becomes, less so with try/catch and more so with functional approaches, how to maintain an error hierarchy structure so that error information can be preserved throughout different ErrorType conformances while still having that descriptive error api.
The best I can come up with so far is:
enum Error: ErrorType {
// Network Errors
case .GetFailed
// Persistence Errors
case .FetchFailed
// More error types
}
which is essentially one long error enum so that any and all errors are of the same type and even the deepest error can be propagated up the chain.
How do other people deal with this? I enjoy the benefits of having one universal error enum but the readability and api clarify suffer. I'd much rather have each function describe what specific error cluster they return rather than have each return Error but again, I don't see how to do that without losing error information along the way.

Just trying a solution to your problem, may not be a perfect one. Using protocol to easily pass around the error objects :
//1.
protocol Error {
func errorDescription()
}
//2.
enum PersistenceError: ErrorType, Error {
case CreateFailed([String: AnyObject)
case FetchFailed(NSPredicate?)
func errorDescription() {
}
}
//3.
enum NetworkError: ErrorType, Error {
case GetFailed(AnyObject.Type) // where AnyObject is your Group model class
func errorDescription() {
}
}
//5.
func fetch() -> SignalProducer<Group, Error> {
...
}

Related

Returning void in PromiseKit 6

This is what I had working with PromiseKit 4.5
api.getUserFirstName().then { name -> Void in
print(name)
}
getUserFirstName() returns a Promsise<String>. I updated to PromiseKit 6 and this now throws an error:
Cannot convert value of type '(_) -> Void' to expected argument type '(_) -> _'
This error message makes little sense to me. How do I fix this?
EDIT: So this seems to fix it, but I have little understanding as to what's happening with this:
api.getUserFirstName().compactMap { name in
print(name)
}
What's the difference now between then() and compactMap()?
In according with PromiseKit 6.0 Guide then was split into then, done and map
then is fed the previous promise value and requires you return a promise.
doneis fed the previous promise value and returns a Void promise (which is 80% of chain usage)
map is fed the previous promise value and requires you return a non-promise, ie. a value.
Why that was happend? As said developers:
With PromiseKit our then did multiple things, and we relied on Swift to infer the correct then from context. However with multiple line thens it would fail to do this, and instead of telling you that the situation was ambiguous it would invent some other error. Often the dreaded cannot convert T to AnyPromise. We have a troubleshooting guide to combat this but I believe in tools that just work, and when you spend 4 years waiting for Swift to fix the issue and Swift doesn’t fix the issue, what do you do? We chose to find a solution at the higher level.
So probably in your case needs to use done
func stackOverflowExample() {
self.getUserFirstName().done { name -> Void in
print(name)
}
}
func getUserFirstName() -> Promise<String> {
return .value("My User")
}
compactMap lets you get error transmission when nil is returned.
firstly {
URLSession.shared.dataTask(.promise, with: url)
}.compactMap {
try JSONDecoder().decode(Foo.self, with: $0.data)
}.done {
//…
}.catch {
// though probably you should return without the `catch`
}
See more info at release guide
compactMap was renamed to flatMap see discussions here

Swift 3 - Protocol with static methods, instance method with generic types, optional '.Type' parameter

I'm trying do implement some networking code that will get json at an endpoint, then parse the result into CoreData objects.
I defined a protocol JSONParsable that some of my CoreData object classes conform to.
import SwiftyJSON
import CoreData
protocol JSONParsable {
// you validate the json response from the server to make sure it has everything you expect to function properly
static func validate(_ jsonResponse: JSON) throws
// now parse the jsonResponse
static func parseAll<T: NSManagedObject>(_ jsonResponse:JSON, into context:NSManagedObjectContext) -> [T]!
// describes how to parse one data object in a json response
static func parse<T: NSManagedObject>(_ jsonObject:JSON, into context:NSManagedObjectContext) -> T?
}
Why? Because they are utility methods for objects of that type. So the "GET Objects" method on my networking client "Manager" looks basically like this:
private func get<T:JSONParsable>(_ urlString: URLConvertible, expectedObjectType:T.Type?, completion: #escaping (_ success: Bool) -> Void)
The problem is, if expectedObjectType is ever nil, the compiler complains with an error:
Generic parameter 'T' could not be inferred
I'm not sure how to solve this. I guess I've taken a wrong approach somewhere, being still from the Objective-C world. Would appreciate some help, including a different approach if necessary.
I am basically saying "get json at a specific URL and use a specific class extension to parse it."
I'm also thinking I could implement a 'Dummy Class' and provide that? Seems a little dirty, but aren't we all a little dirty from time to time? :)
The answer is not exactly an answer, only to say I changed the approach. I made an explicit parser object, changed the protocol definition to instance methods, and the parser object remains a member of a 'service' object, that belongs to the networking client.
It works well this way because json parsing relates to the endpoint that provides that JSON.

Swift: Should NSError now be considered as legacy?

The docs and popular blogs suggest Swift error handling be done with do-catch and to handle an ErrorType enum or an NSError instance.
Are ErrorType enum and NSError instances mutually exclusive in a try catch block? If not, how do you implement a function that throws both?
I have associated an NSError instance to an enum like so, which seems to work, but is this the de facto way of returning detailed error information?
enum Length : ErrorType {
case NotLongEnough(NSError)
case TooLong(NSError)
}
func myFunction() throws {
throw Length.NotLongEnough(NSError(domain: "domain", code: 0, userInfo: [NSLocalizedFailureReasonErrorKey: "Not long enough mate"]))
}
do {
try myFunction()
} catch Length.NotLongEnough(let error) {
print("\(error)")
}
This example shows how ErrorType can be cast to NSError.
do {
let str = try NSString(contentsOfFile: "Foo.bar",
encoding: NSUTF8StringEncoding)
}
catch let error as NSError {
print(error.localizedDescription)
}
I can't find an error enum that conforms to ErrorType for NSString so should we assume it will be an NSError instance ? Granted we could run the code to be sure, but surely the docs should let us know. (I appreciate I might have mis-read the docs)
Any ErrorType can be successfully cast to an NSError, which means that if you want to handle ErrorType as a priority, or specific ErrorType conformant Swift types, you should have those cases before a case where you cast to NSError (which, again, will succeed for all ErrorType conform ant types).
I'm not sure if there are any canonical opinions expressed by Apple at this stage on how to deal with this duality right now, but personally I try to stick to ErrorType conforming Swift types for my own errors and only involve casting to NSError when I want to make conditional behaviour based on some Cocoa or 3rd party NSError based code where the domain & code combo is somehow meaningful to my logic (for instance if specific errors can be filtered out from either logging or from responses).
Problems with the existing facilities in Swift for error handling arise mostly from not being able to tell what kind of errors are thrown by a method, combined with ErrorType lacking some key contextual information included in NSError that gives extra bespoke work every time you want to present ErrorTypes in UI to the user somehow. Unlike NSError, ErrorType does not have a way to pass UI intended information such as a "reason", or "recovery suggestion", or indeed a "description". It looks like this also will not get addressed yet for Swift 3 (there has been some recent discussion on this on the Swift development mailing list).
Regarding "I can't find an error enum that conforms to ErrorType for NSString" I'm really not sure I understood this phrase correctly or whether it is worded correctly overall, but maybe the following is relevant:
I personally follow the convention of making my ErrorType implementations CustomStringConvertible and using the description property to describe a human (UI intended) readable description of the error. This is by no means perfect and especially on OSX where NSResponder nicely enough gives you the presentError method which makes a very nice and clear error dialog if you fill in the description & recovery suggestion info.
NSError class adopts ErrorType interface and any ErrorType-conformant class can be casted to NSError. These features are described here in the docs.
You can safely stick to ErrorType, especially if you're planning to interoperate with Swift only.
enum CommonError: ErrorType {
case InternalInconsistency(String)
}
func boom() throws {
throw CommonError.InternalInconsistency("Boom!")
}
do {
try boom()
} catch {
print(error) // InternalInconsistency("Boom!")
print(error as NSError) // Error Domain=CommonError Code=0 "(null)"
}
do {
try boom()
} catch let CommonError.InternalInconsistency(msg) {
print("Error: \(msg)") // Error: Boom!
}

Swift Error Handling - Struct/Class vs Enum

I am trying to model the possible errors that can be returned by the API I'm consuming and also possible network errors.
I'm struggling to figure out if I should use the Struct/Class approach, where each type of error is modeled as a Struct/Class, or the Enum approach.
Here are some notes to take into consideration:
Depending on what goes wrong, the same API call can return different errors with different media-types (so different JSON). So my errors have different properties. I don't want to expose the request response as a simple JSON Object / Dictionary.
I want the errors caused by a Client/Server issue to have a httpStatusCode property as well (it will be nil for errors that are related to no connectivity)
I want to have an error that indicates a connectivity issue with a proper localized message.
I want to use the same errors for both sync calls (used with throws) and async calls (used as a parameter in the completion block)
Mainly because of the first requirement it's pretty hard to figure out how to go down the Enum way. But maybe I'm missing something, maybe there still is some power that lays in the Enum types that I am not aware of.
Any suggestions would be highly appreciated.
Often enum is the best approach to deal with errors on Swift. To deliver different types you can use associated values and for a localized description you can implement LocalizedError protocol for your enum e.g:
enum NetworkError : Error, LocalizedError {
case noInternet
case httpStatus(Int)
case unknown(Error)
public var errorDescription: String? {
switch self {
case .noInternet:
return "No Internet"
case .httpStatus(let code):
return "HTTP status code: \(code)"
case .unknown(let error):
return "Error: \(error)"
}
}
}
let err1 = NetworkError.noInternet
print(err1.localizedDescription)
let err2 = NetworkError.httpStatus(404)
print(err2.localizedDescription)
let err3 = NetworkError.unknown(error)
print(err3.localizedDescription)
/*
Prints:
No Internet
HTTP status code: 404
Error: error
*/
But if you have common data for your errors you should implement struct that holds needed properties with a type of your error:
struct ResponseError: Error {
enum ErrorType {
case badStatusCode
case noJson
case invalidJson(Error)
}
let type: ErrorType
let statusCode: Int
}
let err = ResponseError(type: .badStatusCode, statusCode: 404)
Read more from the documentation about: https://developer.apple.com/documentation/swift/error
Depending on what your end goal is (a little unclear to me), you can do combinations of Struct/Class, and Enum.
E.g. you could create a Struct that holds your information, and then create an Enum which then has a value of that Struct. Plus you can create functions in your Enum, if needed. There's a lot of functionality in Enums in Swift.
If you would like, you can also make a specific Enum value dependent on another Enum, or even change which Struct/Class each value is dependent on.
For more information on Enums look at apples documentation.

How do I "generify" a closure type alias in Swift?

In order to make my code easier to read, I am using type aliases in Swift for various types of closures. I have the following basic set of closures:
public typealias FailureClosure = (error: NSError?) -> Void
public typealias ProgressClosure = (progress: Float32) -> Void
public typealias BasicClosure = () -> Void
I would like to add a closure typealias that supports generic arrays, but I can't seem to figure out the syntax for it. This is as far as I am able to get, but I get the compile time error "Use of undeclared type 'T'"
public typealias ArrayClosure = <T>(array:[T]?) -> Void
Does anybody know how to do this? Or even if it is possible?
No, this is not currently possible. If it were possible, the syntax you'd expect would be:
public typealias ArrayClosure<T> = (array:[T]?) -> Void
You would then use it like ArrayClosure<Int>. But it's not currently legal.
That said, I don't really recommend these type aliases. They obscure more than they illuminate. Compare this signature:
func foo(onError: FailureClosure)
with:
func foo(onError: NSError? -> Void)
To save just a couple of characters, you force the caller to guess what FailureClosure passes. The error or progress tags don't really help you (you still need to use progress in ...).
The one case that makes a lot of sense is around progress, but I think the type you want there is:
public typealias Progress = Float32
Don't get me wrong here, type aliasing can be very helpful when it creates a new type. Progress is a type that happens to be implemented as as float. But much of what you're doing (definitely with ArrayClosure, and to a lesser extent with the others) is just creating new syntax without creating a new type, and that is more often confusing than helpful.
To call out your specific example, and why overuse of type aliases can cause you to overcomplicate your design:
func foo(failure: ((error: NSError?) -> ())? = nil)
You're right that this is really complicated. Compare:
func foo(failure: NSError -> Void = {_ in return})
Two big changes here. There's no reason to have a failure block that takes an optional error. Always pass an error (if there's no error, why would failure be called?). And there's no reason for the failure block to be optional. If you really want a default value, just make the default value do nothing. Two optionals gone, and all the consuming and implementing code gets simpler. Always think carefully about whether something absolutely must be optional. Optionals add a lot of complexity; don't add them lightly.
Personally, I'd probably do this with an overload instead in many cases:
func foo(#failure: NSError -> Void) { ... }
func foo() {
foo(failure:{ _ in return })
}
I just think it's a little easier to understand what's going on. But either way is fine.
EDIT (Dec 2014): After writing Swift for a few more months, I've grown more fond of #David's approach in the comments below, which is to use an optional for the closure, but not for the error. Particularly given Swift's optional chaining syntax (failure?()), it often does wind up being clearer.
func foo(failure: (NSError -> Void)? = nil)
Swift 4.1 supports generic type aliases. You can use this feature to provide a name for function with generic parameters.
You may have to use such declaration:
public typealias ArrayClosure<T> = ([T]?) -> Void

Resources