Is there an intended use case for sync/async method overload?

I have a 'store' class which dispatches actions. The dispatch is async because in some cases it’s very useful to wait until all the side effects of dispatching an action are complete (eg. for testing). But in normal use, the dispatch method is fire-and-forget.

To avoid the extra call site Task {} boilerplate I found I could add a second non-async method, which I thought would give callers the option of whether to await the dispatch or not, depending on their needs.

@MainActor public final class Store {
    public func dispatch(_ action: Action)  {
        Task { try await _dispatch(action) }
    }

    public func dispatch(_ action: Action) async throws {
        try await _dispatch(action)
    }

    // ...
}

But it seems like I’m misunderstanding how the sync/async method resolution is designed. It seems that from an async context I’m forced to use the async dispatch method? For example, the following fails to compile:

@MainActor func testSyncDispatch() async throws {
    Store().dispatch(SomeAction()) // Expression is 'async' but is not marked with 'await'
}

I’m guessing I just have the wrong mental model with this. Am I missing something obvious? Is there a better way to structure this API?

Overload is chosen based on the call context. If you call method from async function, then async overload will be used. Similarly, if you call from the synchronous context, sync overload will be used.

2 Likes

In case anyone arrives at this thread looking for a quick fix to call a synchronous function from an async function: wrap your function call in a closure:

func someFunc() { }
async func someFunc() { }

Task {
  await someFunc() // calls the async version
  { someFunc() }() // calls the sync version
}
3 Likes