swiftcodableurlsessionurlrequest

Swift do/try/catch - accessing API response in catch block


Say I'm making an API call like this:

Task {
    do {
        let request: Request<LoginResponse> = .postLogin(email: email, password: password)
        let response = try await URLSession.shared.decode(request)
        // store token if received
    } catch {
        // ???
    }
}

struct LoginResponse: Decodable {
    let token: String
    let msg: String
}

extension URLSession {
    func decode<Value: Decodable>(
        _ request: Request<Value>,
        using decoder: JSONDecoder = .init()
    ) async throws -> Value {
        let decoded = Task.detached(priority: .userInitiated) {
            let (data, _) = try await self.data(for: request.urlRequest)
            try Task.checkCancellation()
            return try decoder.decode(Value.self, from: data)
        }
        return try await decoded.value
    }
}

In the catch block of the Task, how would I access the "msg" from LoginResponse? It's an error message being returned from the backend that the login info is incorrect. Or how would I restructure the do/catch so that I can access the LoginResponse object in the catch block?

For further info on the Request object, see here


Solution

  • If your backend still gives you an HTTP response if the login failed, then try await self.data(for: request.urlRequest) won't throw an error. It will return a HTTPURLResponse (which you completely ignored with _) with a statusCode indicating an error.

    self.data(for: request.urlRequest) would only throw when there is no response at all, like when you used an invalid URL, or there is no internet connection.

    Therefore, you can return the HTTPURLResponse back to the caller, and check it in the do block:

    func decode<Value: Decodable>(
        _ request: Request<Value>,
        using decoder: JSONDecoder = .init()
    ) async throws -> (Value, HTTPUURLResponse?) {
        let decoded = Task.detached(priority: .userInitiated) {
            let (data, response) = try await self.data(for: request.urlRequest)
            try Task.checkCancellation()
            return try (
                decoder.decode(Value.self, from: data), 
                response as? HTTPURLResponse // this cast will succeed if the request is an HTTP request
            )
        }
        return try await decoded.value
    }
    
    let request: Request<LoginResponse> = .postLogin(email: email, password: password)
    let (decodedData, response) = try await URLSession.shared.decode(request)
    if let httpResponse = response, httpResponse.statusCode >= 400 {
        // handle error with decodedData...
    }
    

    If you want to handle it in the catch block instead, you can check this in the task, and throw an error instead.

    func decode<Value: Decodable, Failure: Error & Decodable>(
        _ request: Request<Value>,
        using decoder: JSONDecoder = .init(),
        errorResponseType: Failure.Type
    ) async throws -> Value {
        let decoded = Task.detached(priority: .userInitiated) {
            let (data, response) = try await self.data(for: request.urlRequest)
            try Task.checkCancellation()
            if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode >= 400 {
                throw try decoder.decode(errorResponseType, from: data)
            }
            return try decoder.decode(Value.self, from: data)
        }
        return try await decoded.value
    }
    
    // these could be the same type, but I prefer separating errors from successful responses
    struct LoginResponse: Decodable {
        let token: String
        let msg: String
    }
    
    struct LoginError: Decodable, Error {
        let token: String
        let msg: String
    }
    
    do {
        let request: Request<LoginResponse> = .postLogin(email: email, password: password)
        let decodedData = try await URLSession.shared.decode(request, errorResponseType: LoginError.self)
    } catch let error as LoginError {
        // handle login error...
    } catch {
        // handle other errors...
    }