swiftswiftui

Why would a thrown error not be caught in a SwiftUI view?


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?


Solution

  • 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)
        }
        
    }