Typed throw functions

Sure, but then the unique advantage of throws over Result disappears.

Not really, as you'd still have the other advantages of throws in syntax and code generation. Result has the same issue here.

1 Like

It is just all about semantics sometimes. You might be in a throwing context they you don't want to switch into a Result one just because you cannot achieve same results with throws. This proposal is also about making throws being able to be used as well as Result. Which doesn't nowadays.

For now you would need error type conversions like we said here: https://github.com/minuscorp/swift-typed-throws/blob/master/SE-XXX%20Typed%20throws.md#error-type-conversions

So yes it is just the first step. But I could think of an auto generated enum derived from try uses in a block of code. But yes, just brain storming for now. :smiley:

Hm I do think error type conversion is the big weakness here. Either you'd need combined types like you mentioned, or the explicit type conversion will be just as ugly as the explicit mapping of Result that you set out to solve. And in the case of explicit conversion, you'd still have the issue that the error type in the calling function (callFamily) would have to repeat the error cases of the called functions (callKids etc).

func callFamily() throws -> Family {
    let kids = try callKids()
    let spouse = try callSpouse()
    let cat = try callCat()
    return Family(kids: kids, spouse: spouse, cat: cat)
}

I think an elegant solution for this would have to be part of the proposal, otherwise your own examples aren't really improved by it!

Yes this is the hardest case of all. And I added it yesterday. :smiley: I'm more and more convinced that we need to take error conversions into account when we write this proposal. We need to think it through before updating throws

But I disagree that nothing would improved by it comparing to Result.

See this example:

struct Kids {}
struct Spouse {}
struct Cat {}
struct Family {
    let kids: Kids
    let spouse: Spouse
    let cat: Cat
}

struct KidsError: Error {}
struct SpouseError: Error {}
struct CatError: Error {}

enum FamilyError: Error {
    case kidsError(KidsError)
    case spouseError(SpouseError)
    case catError(CatError)
}

// With `throws`

func callKids() throws /* KidsError */ -> Kids { throw KidsError() }

func callSpouse() throws /* SpouseError */ -> Spouse { throw SpouseError() }

func callCat() throws /* CatError */ -> Cat { throw CatError() }

func callFamily() throws -> Family {
    let kids = try callKids()
    let spouse = try callSpouse()
    let cat = try callCat()
    return Family(kids: kids, spouse: spouse, cat: cat)
}

func callFamilySpecificError() throws /* FamilyError */ -> Family {
    do {
        let kids = try callKids()
        let spouse = try callSpouse()
        let cat = try callCat()
        return Family(kids: kids, spouse: spouse, cat: cat)
    } catch let error as KidsError {
        throw FamilyError.kidsError(error)
    } catch let error as SpouseError {
        throw FamilyError.spouseError(error)
    } catch let error as CatError {
        throw FamilyError.catError(error)
    }
}

// With `Result`

func callKidsResult() -> Result<Kids, KidsError> { Result.failure(KidsError()) }

func callSpouseResult() -> Result<Spouse, SpouseError> { Result.failure(SpouseError()) }

func callCatResult() -> Result<Cat, CatError> { Result.failure(CatError()) }

func callFamilyResult() -> Result<Family, FamilyError> {
    return callKidsResult()
        .mapError { error in FamilyError.kidsError(error) }
        .flatMap { kids in
            callSpouseResult()
                .mapError { error in FamilyError.spouseError(error)
            }.flatMap { spouse in
                callCatResult()
                    .mapError { error in FamilyError.catError(error) }
                    .map { cat in
                        Family(kids: kids, spouse: spouse, cat: cat)
                }
        }
    }
}
2 Likes

Personally I prefer untyped errors after 8 years in Java. Simple example: you change something in the implementation and lets say now can throw some file system error. But for some reason you can not update the protocol that limits you in types of Errors you can throw. So you start to either throwing some kind of unknownError, or trying to find the way to wrap it somehow and propagate it up. If you need something descriptive - use Result. Throws basically means that Anything can go wrong, so deal with that accordingly.

2 Likes

Personally I prefer untyped errors after 8 years in Java.

Thanks for your view on this. I heard this conclusions a lot from the Java community. What I don't understand:

If you need to throw it, because you think your API user needs to know, that a FileSystemError did happen, then it's a breaking change, because your API user may want to react to it in another way now.

If you don't need to throw it, because you think your API user does not need to know it, then you map it to the error that represents the FileSystemError.

If you think you don't know what will happen in the future, then just throw Swift.Error. But maybe I don't use your API then because you are not clear enough about what can happen. Or I will just do a general catch because nothing else is guaranteed.

Did I forgot a case?

2 Likes

Some examples, why sometimes we prefer Result over type erased error is working with network errors. I can show some examples:

/// Allows us to enumerate all erros
public enum NetworkError: ConcreteBaseError {
  case urlSessionError(error: URLSessionError, response: DataTaskHttpResponse?)
  case httpStatusError(error: HttpStatusError, response: DataTaskHttpResponse)
  ///...
}

/// Allows us to switch over errorCode
public final class HttpStatusError: ConcreteBaseError {
  public let errorCode: HttpStatusCode

  ...
}

public enum Http400StatusCode: Int, HttpStatusCodeType {
  case badRequest = 400
  case unauthorized = 401
  case paymentRequired = 402
  // ... 
}

But the best benefits come when handling domain errors. The list of these error codes is finite. We use an exhaustive switch statement. It is very important, that if the list of error codes changes, the compiler help us to handle all pieces of code that rely on these error codes.

public enum NetworkDomainableError<DomainCode: DomainCodeType>: ConcreteBaseError {
  /// The usual network layer error
  case networkError(NetworkError)
  
  /// The domain error from the backend. It is not a kind of usual Network error. It is a kind of business logic error
  case domainError(DomainCode)
}

/// Checkout domain codes from backend
enum CheckoutNewOrderErrorCode: String, DomainCodeType {
  case orderPriceMoreThanMaximum = "order_price_more_than_maximum"
  case limitationsNotSet = "limitations_not_set"
  case orderPriceLessThanMinimum = "order_price_less_than_minimum"
  // full list is much biger ...
}

Note, that each error code leads to completely different behavior. We can close the screen, or offer the user to chgande the payment type, or notify the user that some product in his cart is not available anymore ...

Dealing with type erased error in such cases is a nightmare.

Some more examples, that are not connected with networking. These errors are created in local app services / managers / controllers:

func refreshThrottlingResult() -> Result<Void, ThrottlingError>

func validate(_ text: String) -> Result<Void, EmailValidationError>

func asDomainResult(dto: Self) -> Result<Darkstore.SearchItem, MappingError>
// in some cases, MappingError is sent to Crashlytics

Though, we use throwable functions a lot. For example, when dealing with Keychain, thrown Error is enough. We don't care what exactly this error is.

3 Likes

Seems to me like a good example of how you prefer Result over throws just because you can type the throw that you're making. That shouldn't be like that.

I don't really understand the pain here. Why can't you just cast at runtime and then switch over the result?

if let error = error as? MyDomainError {
  switch error {
    case ...
  }
}

You still get exhaustiveness checking.

That's why: https://github.com/minuscorp/swift-typed-throws/blob/master/SE-XXX%20Typed%20throws.md#potential-drift-between-thrown-errors-and-catch-clauses

1 Like

The poster I responded to was speaking specifically of domain errors. I find it unlikely that their developers would forget to check their own types.

Then likeliness hit us too often in some of my projects (especially in more complex projects). Your experience may vary and you should not be forced to use explicit errors, but we want to have the possibility to use them.

2 Likes

One of the older posts described that problems in the other way can also happen. If a later implementation of a Java function purges an exception type, you still have to keep that type in the function's signature's exception list forever to maintain backwards compatibility.

The desired async/await feature has an interaction model that is very similar to throws/try. So much so that sometimes we wonder if we should have a general effects feature and put a/a, and move t/t, under that. How would typed-throwing work as an effect? If we want both, maybe we should implement typed-throwing after general effects.

3 Likes

In Java, typed exceptions are especially bad since you are encouraged to use interfaces as an extensibility method, such as to integrate a third party system. An implementation that returns data from the network will have network-related errors, but the interface itself most likely does not define those as potential exception types.

In Java 1.4 they added the inner exception concept to built-in exceptions to deal with this pain point, since without wrapping the exception you'll lose context of what actually went wrong. However, the problem still exists that a lot of third party exceptions do not expose the ability to initialize with an innerException, so you are forced to figure out how to translate your error into the built-in system, and potentially lose the ability to detect and report issues outside the interface - for instance, see that there was an authentication error if you supply a JSON parser with data via a HTTP client.

Just the reportability of what went wrong can't necessarily be represented in the error types exposed by the interface, which IMHO is one of the reasons that a cross-cutting logging solution became essential even for libraries.

Swift will have this problem worse than Java if we do this wrong - there is no way to extend a concrete Error subtype from a third-party library to add 'innerError' / 'cause' information.

1 Like

Maybe we could have a new protocol, PossiblyWrappingError, that refines Error. It defines an access point for an inner-cause (Optional) error. Typed-throws would require the explicitly-listed error types to conform to the new protocol.

I'm a little bit -0.5 to this typed-throw design conception.

We already have Typed Result<Data, Error> and Untyped throws -> Data why we need another typed throws Error -> Data approach, I know maybe it's helpful in some cases, but overall there's NO huge benefits/changes for developer to deal with exception handling.

Otherwise, if using throws Error -> Result<Data, AnotherError> pattern, is it meaningful or just a way of duality of error propagation.

1 Like

async and throws are both monadic, but having separate keywords is more readable from my first intuition