34

I have a function that throws an error, in this function I have a inside a closure that I need to throw the error from it's completion handler. Is that possible ?

Here is my code so far.

enum CalendarEventError: ErrorType {
    case UnAuthorized
    case AccessDenied
    case Failed
}

func insertEventToDefaultCalendar(event :EKEvent) throws {
    let eventStore = EKEventStore()
    switch EKEventStore.authorizationStatusForEntityType(.Event) {
    case .Authorized:
        do {
            try insertEvent(eventStore, event: event)
        } catch {
            throw CalendarEventError.Failed
        }

    case .Denied:
        throw CalendarEventError.AccessDenied

    case .NotDetermined:
        eventStore.requestAccessToEntityType(EKEntityType.Event, completion: { (granted, error) -> Void in
            if granted {
                //insertEvent(eventStore)
            } else {
                //throw CalendarEventError.AccessDenied
            }
        })
    default:
    }
}
Pranav Kasetti
  • 8,770
  • 2
  • 50
  • 71
shannoga
  • 19,649
  • 20
  • 104
  • 169
  • You could store a boolean outside of the `eventStore.` part and change it inside instead of throwing an error and then check the boolean outside and throw an exception is necessary – Arc676 Oct 19 '15 at 11:58
  • 2
    @Arc676 If `completion` closure is called asynchronously then it's not possible because `insertEventToDefaultCalendar` will return before `completion` is called. – mixel Oct 19 '15 at 12:37
  • @shannoga I updated my answer with workaround for your case. – mixel Oct 19 '15 at 13:07

6 Answers6

33

When you define closure that throws:

enum MyError: ErrorType {
    case Failed
}

let closure = {
    throw MyError.Failed
}

then type of this closure is () throws -> () and function that takes this closure as parameter must have the same parameter type:

func myFunction(completion: () throws -> ()) {
}

It this function you can call completion closure synchronous:

func myFunction(completion: () throws -> ()) throws {
    completion() 
}

and you have to add throws keyword to function signature or call completion with try!:

func myFunction(completion: () throws -> ()) {
    try! completion() 
}

or asynchronous:

func myFunction(completion: () throws -> ()) {
    dispatch_async(dispatch_get_main_queue(), { try! completion() })
}

In last case you will not be able to catch error.

So if completion closure in eventStore.requestAccessToEntityType method and the method itself does not have throws in its signature or if completion is called asynchronously then you can not throw from this closure.

I suggest you the following implementation of your function that passes error to callback instead of throwing it:

func insertEventToDefaultCalendar(event: EKEvent, completion: CalendarEventError? -> ()) {
    let eventStore = EKEventStore()
    switch EKEventStore.authorizationStatusForEntityType(.Event) {
    case .Authorized:
        do {
            try insertEvent(eventStore, event: event)
        } catch {
            completion(CalendarEventError.Failed)
        }

    case .Denied:
        completion(CalendarEventError.AccessDenied)

    case .NotDetermined:
        eventStore.requestAccessToEntityType(EKEntityType.Event, completion: { (granted, error) -> Void in
            if granted {
                //insertEvent(eventStore)
            } else {
                completion(CalendarEventError.AccessDenied)
            }
        })
    default:
    }
}
mixel
  • 25,177
  • 13
  • 126
  • 165
8

Because throwing is synchronous, an async function that wants to throw must have an inner closure that throws, such as this:

func insertEventToDefaultCalendar(event :EKEvent, completion: (() throws -> Void) -> Void) {
    let eventStore = EKEventStore()
    switch EKEventStore.authorizationStatusForEntityType(.Event) {
    case .Authorized:
        do {
            try insertEvent(eventStore, event: event)
            completion { /*Success*/ }
        } catch {
            completion { throw CalendarEventError.Failed }
        }

        case .Denied:
            completion { throw CalendarEventError.AccessDenied }

        case .NotDetermined:
            eventStore.requestAccessToEntityType(EKEntityType.Event, completion: { (granted, error) -> Void in
                if granted {
                    let _ = try? self.insertEvent(eventStore, event: event)
                    completion { /*Success*/ }
                } else {
                    completion { throw CalendarEventError.AccessDenied }
                }
        })
        default:
            break
    }
}

Then, at the call site, you use it like this:

   insertEventToDefaultCalendar(EKEvent()) { response in
        do {
            try response()
            // Success
        }
        catch {
            // Error
            print(error)
        }
    }
Rafael Nobre
  • 5,062
  • 40
  • 40
  • `let _ = try? self.insertEvent(eventStore, event: event)` In this line, you're silently removing the error. I'd say it's not the best error handling. – Nat Apr 20 '22 at 09:21
6

That's not possible in this case - that completion handler would have to be declared with throws (and the method with rethrows) and this one is not.

Note that all that throwing is just a different notations for NSError ** in Objective-C (inout error parameter). The Objective-C callback doesn't have an inout parameter so there is no way to pass the error up.

You will have to use a different method to handle errors.

In general, NSError ** in Obj-C or throws in Swift don't play well with asynchronous methods because the error handling works synchronously.

Sulthan
  • 128,090
  • 22
  • 218
  • 270
0

You can not make function with throw, but return a closure with status or error! If it's not clear I can give some code.

katleta3000
  • 2,484
  • 1
  • 18
  • 23
0

requestAccessToEntityType does its work asynchronously. When the completion handler is eventually run your function already returned. Therefore it is not possible to throw an error from the closure the way you are suggesting.

You should probably refactor the code so that the authorization part is handled separately from the event insertion and only call insertEventToDefaultCalendar when you know the authorization status is as expected/required.

If you really want to handle everyhing in one function, you could use a semaphore (or a similar technique) so that the asynchronous code part behaves synchronously with regard to your function.

func insertEventToDefaultCalendar(event :EKEvent) throws {
    var accessGranted: Bool = false

    let eventStore = EKEventStore()
    switch EKEventStore.authorizationStatusForEntityType(.Event) {
    case .Authorized:
        accessGranted = true

    case .Denied, .Restricted:
        accessGranted = false

    case .NotDetermined:
        let semaphore = dispatch_semaphore_create(0)
        eventStore.requestAccessToEntityType(EKEntityType.Event, completion: { (granted, error) -> Void in
            accessGranted = granted
            dispatch_semaphore_signal(semaphore)
        })
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
    }

    if accessGranted {
        do {
            try insertEvent(eventStore, event: event)
        } catch {
            throw CalendarEventError.Failed
        }
    }
    else {
        throw CalendarEventError.AccessDenied
    }
}
iOSX
  • 1,270
  • 13
  • 18
  • Using `semaphore` is really bad idea because `insertEventToDefaultCalendar` and `completion` closure are called on the same thread and this thread will be blocked after `dispatch_semaphore_wait` so `completion` and other code will never be called. – mixel Oct 19 '15 at 13:37
  • Test on the device was also successful. – iOSX Oct 19 '15 at 14:06
  • I tested this code https://gist.github.com/mxl/dc501fd93074dda110ff in playground and as OS X command line tool project. It prints "1" but never prints "2". – mixel Oct 19 '15 at 14:08
  • Tested it on iOS device - same result, it never prints "2". – mixel Oct 19 '15 at 14:13
  • I can confirm that your code does indeed block the thread, but if you test my code snippet, you will notice that it works just fine ;) – iOSX Oct 19 '15 at 14:16
  • I tested your code snippet. I set breakpoints at `accessGranted = granted` and `if accessGranted {` lines and execution never stops on them if I call `insertEventToDefaultCalendar` from `UIViewController.viewDidLoad`. – mixel Oct 19 '15 at 14:29
  • It does not even show dialog with access request until I stop debugging session and kill application. – mixel Oct 19 '15 at 14:35
  • As said before, it works fine for me. Anyway, I've modified your gist so that the completion handler is executed on another queue and then it works too, i.e. "2" is printed. http://pastebin.com/E3rcC9NJ – iOSX Oct 19 '15 at 15:01
  • It's interesting how you made it work fine. Can you provide me a link to source of your sample project? I know the trick with executing completion handler on another queue but you can not do this with `eventStore.requestAccessToEntityType`. – mixel Oct 19 '15 at 15:06
  • Here is the sample project as requested by you: https://www.dropbox.com/s/uj6as25n14roh9m/eventkit%20semaphor.zip?dl=0 – iOSX Oct 19 '15 at 15:22
  • Yes, it works because `completion` handler is called on an arbitrary queue as said in docs https://developer.apple.com/library/ios/documentation/EventKit/Reference/EKEventStoreClassRef/ Arbitrary means that it can be called on main queue and in this case it will wait for semaphore forever. – mixel Oct 19 '15 at 18:25
0

Simple solution

While it is possible with a lot of boilerplate to achieve this via nested closures, the amount of work needed to achieve something this simple is a lot.

The below solution doesn't solve the problem technically but offers a more suitable design for the same problem.

Zoom out

I believe error handling is better captured via a type instance variable self.error, that we update asynchronously and reactively respond to.

We could achieve reactive updates either through @Published with ObservableObject in Combine or via delegates and didSet handlers natively. The same principle of error handling reactively regardless of technique I feel fits this problem better and is not over-engineered as much.

Example code

class NetworkService {

    weak var delegate: NetworkDelegate? // Use your own custom delegate for responding to errors.

    var error: IdentifiableError { // Use your own custom error type.
        didSet {
            delegate?.handleError(error)
        }
    }

    public func reload() {
        URLSession.shared.dataTask(with: "https://newsapi.org/v2/everything?q=tesla&from=2021-07-28&sortBy=publishedAt&apiKey=API_KEY") { data, response, error in
            do {
                if let error = error { throw error }
                let articles = try JSONDecoder().decode([Article].self, from: data ?? Data())
                DispatchQueue.main.async { self.articles = articles }
            } catch {
                DispatchQueue.main.async { self.error = IdentifiableError(underlying: error) }
            }
        }.resume()
    }
}

Note

I am writing prior to async / await in Swift 5.5, which makes this problem a lot easier. This answer will still be helpful for backporting < iOS 13 because we need to use GCD.

Pranav Kasetti
  • 8,770
  • 2
  • 50
  • 71