Typed throw functions

I'll think about the example you gave me and see if it fits in the current proposal pitch, I think with your example it gets pretty clear! Thank you!

Well I didn‘t meant to showcase a scenario that would throw multiple errors at once with something like a OneOf type, which is inspired by Either but contains more cases. All I was reusing / rehashing from the rejected topic was the shortcut syntax which used the | operator directly on types. Int | Bool !== Bool | Int as OneOf<Int, Bool> !== OneOf<Bool, Int> similar to Either<Int, Bool> !== Either<Bool, Int>. That is enough off-topic for this thread and we shouldn‘t dive deeper into that topic. Long story short all I wanted to express in context of typed throws that for me the T | R syntax could mean a variadic Either like type. In my opinion it would significantly help in some situations where you want to recover from one of many potential strong error types that gets thrown from the same scope.

1 Like

We’ve heard from a lot of titans, my simple opinion is that optionally adding a type to throws will make some kinds of code a lot cleaner and since it’s optional it won’t affect other code.

For example, in the online store I wrote to sell Delicious Library, I fire credit card info off to PayPal PayFlow Pro (née VeriSign) and it returns an error code (if any). This domain is extremely well-known and the method that calls PayFlow’s fetch is much cleaner if I can tell future me, “Hey, this’ll throw a Payflow error only” on the fetch method.

In this case I do handle every single type of error that is thrown in a unique way — a timeout is very different from a CVC typo or a fraud alert. (For completeness I should mention that actually CVC and credit card # and name mismatches are all handed with the same exact error message in practice to add more surface against brute force attacks.)

-Wil

6 Likes

Despite the similar keywords, Swift error throwing corresponds to the Cocoa error handling convention, not to Objective-C exceptions. So you should compare throws to error:(NSError **)error parameters, not to the undeclared exception handling behavior of Objective-C methods.

2 Likes

We're close to have a well-written pitch, so if someone has more content that thinks that should be able, this is the moment!

1 Like

What about putting it there: Question/Idea: Improving explicit error handling in Swift (with enum operations)

For me this is connected to the topic I opened up over there.

Besides explicitly specifying what can go wrong, typed throws can be very useful when there is some special custom error type in code (application, library, cli tool), that top level code knows how to display to user. For example, writing a command line utility, it's nice to write to console what went wrong:

try {
  runTool(args)
} catch let myErr as MyError {
  processError(myErr)
} catch {
  print(error)
}

The problem with this approach that every once in a while some NSError from some file operation is sneaking into default catch, producing bad, reflection based description. Localised description doesn't help either, because it lacks important information (for example "No such file or directory" error gives only filename, no path information).

This problem exists, for example, in swift-driver, requiring tedious tracking of all "foreign" errors, without typecheker to help (I don't know how big is an issue it is there, but I wrote all my cli tools in same style, with do-catch clause at top level).

I'm not saying that it should be fixed with typed errors there, it's always a trade between profit and amount of boilerplate it requires, but it would be nice to, at least, have an option to move this tedious work to typecheker.

By far the most useful and interesting aspect of throw in my mind is that errors thrown at different levels in a nested call structure are gathered up and exposed in the same place. "Railroad oriented programming" automatically. How would that play with typed errors?

func a() throws ??? {
    if ... { throw TypeAError.something }
    try b()
}

func b() throws TypeBError {
    if ... { throw TypeBError.somethingElse }
}

Without this railroad aspect of throw is it really much different from using Result?

I think one obvious solution be to either propagate the type (throws SomeError), in which case calling functions that don't throw that error would be a compiler error, or the type would automatically be erased at the first "car" that doesn't type its error (throws).

1 Like

As far as I understand your issue, you would need to wrap this API and convert to specific type checked errors to move more information into the type system. How much information that would be is your decision. But yes first we need to have the possibility to do that. :slight_smile:

If I understand you correctly, that wouldn't cover the case I'm talking about, what you describe sounds like how normal return types work, but errors are different, they can propagate several call levels at once, like in my example. Calling a() could yield either a TypeAError or a TypeBError-

Good observation. I mentioned some aspects of it here: https://github.com/minuscorp/swift-typed-throws/blob/master/SE-XXX%20Typed%20throws.md#result-is-not-the-go-to-replacement-for-throws-in-imperative-languages

But your specific point came up some months ago when I used Result and I started thinking about a solution, so maybe this post is for you: Question/Idea: Improving explicit error handling in Swift (with enum operations)

This proposal is the first step in making explicit errors with throws more usable. As some commenters already pointed out there is a challenge in applying error conversions/accumulations (maybe inferring them). But let split that up and discuss the error converting specifics in the post I mentioned above.

2 Likes

If you wrap your try b() you could handle its error and propagate the same error that you were raising, but that's up to how you'd want to do things.

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