Effect that can fail?

So, I'm writing an application that performs calls to an external service. In trying to model this with Effect, I've found plenty of examples on how to do this if the call can never fail. But I don't want to provide a reasonable default in the case where the user's login credentials were refused by the service; I think that's a really good case for a failure result.

Given that what I think I want is an Effect<AuthToken, Error>, how do I make this work?

Effect.task {
  do {
    let authToken = try await possiblyFailingAPICall(credentials)
    return authToken // the success case, obvs.
  } catch {
    print("Login failed.") // what do I return here?
  }
}

I should clarify this a bit. In simplifying the example, I think I erased some important stuff. So, here's the longer, actual code:

enum APIError: Error, Equatable {
    case malformedHostURL(String),
         operationFailed(String),
         dataCorrupted(String),
         missingInformation(String)
}

struct AuthClient {
    enum WebAPIError: Error, Equatable {
      case identityTokenMissing
      case unableToDecodeIdentityToken
      case unableToEncodeJSONData
      case unableToDecodeJSONData
      case unauthorized
      case invalidResponse
      case httpError(statusCode: Int)
    }

    struct SIWAAuthRequestBody: Encodable, CustomStringConvertible {
      let name: String?
      let email: String?
      let appleIdentityToken: String

        var description: String {
            "name: \(name ?? "nil"), email: \(email ?? "nil"), appleIdentityToken: \(appleIdentityToken)"
        }
    }

    var signInWithApple: (String?, String?, Data?) -> Effect<AuthContent, Error>
}

extension AuthClient {
    static let serviceHost = "http://localhost:8080"

    static let live = Self(
        signInWithApple: { name, email, identityToken in
            Effect.task {
                guard let identityToken = identityToken else { throw WebAPIError.identityTokenMissing }

                guard let identityTokenString = String(data: identityToken, encoding: .utf8)
                else { throw WebAPIError.unableToDecodeIdentityToken }

                let body = SIWAAuthRequestBody(name: name,
                                               email: email,
                                               appleIdentityToken: identityTokenString)

                guard let jsonBody = try? JSONEncoder().encode(body) else { throw WebAPIError.unableToEncodeJSONData }
                guard let url = URL(string: "\(serviceHost)/v1/siwa/auth") else { throw APIError.malformedHostURL(serviceHost) }
                var request = URLRequest(url: url)
                request.httpMethod = "POST"
                request.addValue("application/json", forHTTPHeaderField: "Content-Type")
                request.httpBody = jsonBody
                do {
                    let authContent: AuthContent = try await session.decode(for: request, dateDecodingStrategy: .iso8601)
                    return authContent
                } catch {
                    throw APIError.operationFailed(error.localizedDescription)
                }
            }
            .setFailureType(to: Error.self)
            .eraseToEffect()
        }
    )
}

The compiler complains, saying, "invalid conversion from throwing function of type '@Sendable () async throws -> AuthContent' to non-throwing function type '@Sendable () async -> AuthContent'"

I'm not entirely sure what that means. Does it mean that I need to refactor the error types so that the closure can only throw a single type? It reads as though the closure isn't allowed to throw anything. Or something.

You can just throw there any error you want. Or you can avoid the catch and simply let the original error bubble up:

Effect.task {
  try await possiblyFailingAPICall(credentials)
}

Why .setFailureType(to: Error.self)? It should already use this overload, so it’s already Error. Maybe that’s the culprit.

1 Like

Yeah @victor is correct, the .setFailureType(to:) method is only defined when Failure == Never, and your effect does have a failure. So removing that should fix things.

1 Like