I'm trying to troubleshoot a situation where I'm throwing errors from a view model for a SwiftUI app and they are not being caught at the view level. I can see that they are successfully being thrown up from the network layer to the view model. But they are not being caught in the view as far as I can see. I'm hoping somebody can spot what I'm doing wrong and suggest a fix.
Here's the method in my view model that throws the error, in this case set up specifically to print a message when it catches an error (for a case where I can repeatedly make sure one is produced).
func itemSelected(from provider: any ListViewDataProvider) throws {
resetForType(provider.listType)
Task {
switch provider.listType {
case .application:
try await requestPlatforms()
case .platform:
try await requestAppActions()
case .actions:
do {
try await requestInteractionDetails()
} catch {
print("*** Error caught for provider: \(provider) of type \(provider.listType)") // this always prints when there's an error to throw
throw error
}
case .counters:
try await requestCounterDetails()
case .details:
break
}
}
}
I call it from a method in my view, but when I cause the server error to be thrown, the print statement in the catch clause never appears, (although logging from the network layer does show the error), and a breakpoint on the line current error = error
is never hit.
The listViewModel
variable is a reference to the view model, defined as @Observable class ListViewModel
, that's passed into the view via an Environment declaration.
func handleItemSelection(_ selection: String, dataProvider: any ListViewDataProvider) {
if let index = dataProvider.items.firstIndex(where: { $0.id.uuidString == selection }) {
if let provider = dataProvider as? ActionsListViewModel {
let selectedAction = provider.items[index]
provider.selectedItem = selectedAction
provider.baseWhereClause = "\(Items.description) = \(selectedAction.name.sqlify())"
} else if let provider = dataProvider as? CountersListViewModel {
let selectedAction = provider.items[index]
provider.selectedItem = selectedAction
provider.baseWhereClause = "\(Items.description) = \(selectedAction.name.sqlify())"
} else {
return
}
do {
try listViewModel.itemSelected(from: dataProvider)
} catch {
print("*** Error caught in view") // this never prints
currentError = error
showError = true
}
} else {
return
}
}
Any suggestions for solving this problem?
In func itemSelected(...)
the Task {}
execution is detached
from any outer try, so any errors are blocked inside it.
To capture the error in the View
,
try this approach where you remove the itemSelected Task {...}
wrapping, and
let the functions requestAppActions
and requestInteractionDetails
throw directly.
Example test code:
struct ContentView: View {
@State private var vm = ListViewModel()
var body: some View {
Button("do actions") {
Task {
await handleItemSelection("actions")
}
}.buttonStyle(.borderedProminent)
Button("do platform") {
Task {
await handleItemSelection("platform")
}
}.buttonStyle(.borderedProminent)
}
func handleItemSelection(_ item: String) async {
do {
try await vm.itemSelected(from: item)
} catch {
print("---> handleItemSelection error in view \(error)\n")
}
}
}
@Observable class ListViewModel {
var items: [String] = []
func itemSelected(from provider: String) async throws {
switch provider {
case "platform":
try await requestAppActions()
case "actions":
do {
try await requestInteractionDetails()
} catch {
print("----> itemSelected error \(error)")
throw error
}
default :
break
}
}
func requestAppActions() async throws {
throw URLError(.badServerResponse)
}
func requestInteractionDetails() async throws {
throw URLError(.unknown)
}
}