Video #176: Parser Errors: from Nil to Throws
Episode: Video #176 Date: Jan 31, 2022 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep176-parser-errors-from-nil-to-throws

Description
Let’s explore the topic of error handling. We will scrutinize how we model errors in our parsing library and the problems that have come out of it, and we will address these problems by changing the fundamental shape of the parser type.
Video
Cloudflare Stream video ID: 92aa2bb092793e3c80135a43f21ea276 Local file: video_176_parser-errors-from-nil-to-throws.mp4 *(download with --video 176)*
References
- Discussions
- What is @_disfavoredOverload in Swift?
- 0176-parser-errors-pt1
- Brandon Williams
- Stephen Celis
- Mastodon
- GitHub
- CC BY-NC-SA 4.0
- source code
- MIT License
Transcript
— 0:05
Last week we finished up a series of episodes bringing result builders to our parsing library, and even released a whole new version of the library that converts everything to the new builder style. We showed that the builder syntax removes a lot of noise from building large, complex parsers and even unlocks new styles of API design that were not previously possible.
— 0:23
This week we are going to address another change to the library that will bring many ergonomic and quality-of-life improvements to using it. We are going to demonstrate how to add basic error handling to the fundamental shape of parsers, which will give us an opportunity to better describe what goes wrong when a parser fails. The need for errors
— 0:39
Let’s start by looking at why we want parsing errors compared to what we have today. There are really 2 main reasons: the current model for parsing errors causes some of our parsers’ implementations to be a little more awkward than they need to be, and when a parser currently fails it does not leave behind any contextual information.
— 0:57
Let’s take a look at this first reason.
— 1:02
The way parsers express failability currently is by returning an optional Output : public protocol Parser { associatedtype Input associatedtype Output func parse(_ input: inout Input) -> Output? }
— 1:07
This allows a parser to fail by returning nil .
— 1:09
This can be awkward for some parsers because we often have the concept of a Void parser, which is one that doesn’t need to output anything of value but rather can only succeed or fail. For example, the library ships with parsers that allow you to check if the input begins with a certain value, and if it does it consumes that part of the input and if it does not it fails.
— 1:27
In fact, the various Swift string representations all conform to the Parser protocol with this logic, including String , UTF8View , and more. This means you can use a string literal to parse some beginning characters from an input string: "Hello".parse("Hello world") // ()
— 1:54
Now these parsers don’t return anything of value because all we are doing is checking that the input starts with the string. That’s why this parser returns () if it succeeds and nil if it fails.
— 2:08
Void parsers are handy, but implementing them with optional return values is strange: extension String: Parser { @inlinable public func parse(_ input: inout Substring) -> Void? { guard input.starts(with: self) else { return nil } input.removeFirst(self.count) return () } }
— 2:18
Here we are returning an optional Void value, which is already a pretty odd thing to see, but it also means we need to explicitly return () in order to satisfy the compiler. Void -returning functions are given a lot of affordances that other functions are not given. If we get a basic, Void -returning function in place: func foo() -> Void { return () }
— 2:43
For one thing, you don’t have to return explicitly: func foo() -> Void { /* return () */ }
— 2:50
You don’t even have to specify their return type: func foo() /* -> Void */ { /* return () */ }
— 2:57
And if you do decide to return, like if it makes sense to early out, you can just use a bare return with no value provided: func foo() /* -> Void */ { return /* () */ }
— 3:08
The reason this is possible is because Void only contains one single value, represented by () , and so Swift does the busy work of implicitly inserting the return statement for you so that you don’t have to.
— 3:20
We lose all of those affordances once we return Void? because Swift does not want to assume too much. If it still inserted implicit return () s it could accidentally mask situations that you mean to be returning nil .
— 3:34
And so that’s why we have this silly return () at the end of the parse method. If we search the code base for return () we will find many more examples of this.
— 3:45
Then there are other parsers that deal with optionality themselves and so forcing the return type of the parse to be optional causes a double-optional situation that is hard to wrap one’s mind around.
— 3:58
For example, there is a parser called Optionally , which we have never covered in our episodes but it can turn any parser into one that does not fail. It does this by upgrading the parser to be a parser of optional values, and then if the underlying parser fails it will simply return a .some(nil) to force a success: public struct Optionally<Wrapped>: Parser where Wrapped: Parser { … @inlinable public func parse(_ input: inout Wrapped.Input) -> Wrapped.Output?? { .some(self.wrapped.parse(&input)) } }
— 4:26
So, if we wrap an integer parser in Optionally and then run it on a string with no numeric characters, we get a successful result of .some(nil) : Optionally { Int.parser() } .parse("Hello") == .some(nil) // true
— 4:55
This is already pretty trippy, and if the underlying parser parsed something successfully, we get a double-nested value: Optionally { Int.parser() } .parse("42") == .some(.some(42) // true
— 5:15
Notice that the return type of the Optionally parser is Wrapped.Output?? , which is the cause of what’s making things so trippy.
— 5:23
These little annoyances really add up. Nearly every parser in the library has some strangeness involved due to working with optionals, even when optional Void s are not involved. Take for example the .map operator on parsers, which transforms a given parser into a new parser by allowing you to apply a function to the output of the original parser: public struct Map<Upstream, Output>: Parser where Upstream: Parser { … @inlinable @inline(__always) public func parse(_ input: inout Upstream.Input) -> Output? { self.upstream.parse(&input).map(self.transform) } }
— 5:48
To implement this we first run the upstream parser to get an optional Output value, and then we .map on that optional value in order to to apply the transform function if the output is non- nil .
— 5:56
Said another way, this code is equivalent to us using a guard let to unwrap the optional and then applying the transformation: public func parse(_ input: inout Upstream.Input) -> Output? { guard let output = self.upstream.parse(&input) else { return nil } return self.transform(output) }
— 6:16
Ideally we would just be able to write it like this: public func parse(_ input: inout Upstream.Input) -> Output? { self.transform(self.upstream.parse(&input)) }
— 6:25
This matches how we described the .map operator on parsers out loud. It just means to create a new parser that applies a transformation to the output of the underlying parser.
— 6:33
Beyond the implementation annoyances, which mostly just affects people creating new Parser protocol conformances, there are also annoyances to using the parsing library, and that affects a lot more people.
— 6:44
Right now when parsing fails we don’t know why it failed. We aren’t given any info on what went wrong and where it went wrong. For example, if we run our user parser on a string that has “true” misspelled: struct User { var id: Int var name: String var isAdmin: Bool } let user = Parse(User.init) { Int.parser() "," Prefix { $0 != "," }.map(String.init) "," Bool.parser() } user.parse("1,Blob,tru") // nil
— 7:21
All we see is that something went wrong because nil was returned. This parser is simple enough that perhaps that isn’t a big deal. But parsers can get really complex and inputs can get really big, and so it can become cumbersome to figure out what went wrong when nil is returned. A better way
— 7:39
So, all of this is what led us to consider a change to the fundamental shape of our parser in order to improve the ergonomics of parser conformances, and to improve error messaging when a parser fails.
— 7:55
To do this we will drop the optional from the return type of parse : public protocol Parser { associatedtype Input associatedtype Output func parse(_ input: inout Input) -> Output? }
— 8:00
And instead allow it to throw: public protocol Parser { associatedtype Input associatedtype Output func parse(_ input: inout Input) throws -> Output }
— 8:10
If we do that naively then we get hundreds of compiler errors because all of our existing parsers are no longer implementing the correct method. We’d have to go fix every single one of them right now.
— 8:24
Ideally we could provide a migration path for this style of parsing by having both requirements on the Parser protocol: public protocol Parser { associatedtype Input associatedtype Output func parse(_ input: inout Input) -> Output? func parse(_ input: inout Input) throws -> Output }
— 8:37
And then implement the throwing requirement in terms of the optional return type requirement: struct ParsingError: Error {} extension Parser { public func parse(_ input: inout Input) throws -> Output { guard let output = self.parse(&input) else { throw ParsingError() } return output } }
— 9:29
However this still causes us to have a ton of compiler errors. Since non-throwing functions can satisfying throwing requirements of protocols, it seems that Swift can’t decide if all the existing parser methods we have should satisfying the optional or throwing requirement. To fix this we would literally have to update every single conformance to explicitly spell out each associated type as a type alias.
— 9:52
For example, the Always parsers would become this: public struct Always<Input, Output>: Parser { public typealias Input = Input public typealias Output = Output … }
— 10:26
And now this parser compiles.
— 10:38
It’s a real bummer that this happens because it means we can’t provide a temporary migration strategy for when we update the library to this new style. Luckily it is not very common to write protocol conformances directly, but rather you typically use parsers that come with the library, such as the integer parser, prefix parser, and more, and then you piece them together into more complex parsers using operators, such as .map , Many and more.
— 11:10
So hopefully this migration won’t be too painful for users of our library, and to unblock us we have a git change stashed that we can apply to the project to inserts all of the type aliases.
— 11:35
And now when we build things, it succeeds.
— 11:40
Now everything is compiling, but we want to stress that unfortunately this is not how the library is going to be able to be released. It doesn’t help with backwards compatibility since existing conformances would still have to add explicit type aliases, and so there’s no reason to ship things in this state. But, for the purpose of this episode, it does allow us to incrementally convert parsers over to the new throwing style, so we will go with it for now. Simplifying existing parsers
— 12:07
Now that we have the new parser shape in place, and everything is compiling, let’s see how it can help us with the two problems we outlined earlier. Those problems were: 1.) ergonomics of writing parsers that return optional values, especially optional void values, and 2.) lack of messaging when something fails in a parser.
— 12:31
Let’s start with the first. Previously we looked at a few void parsers to see how awkward they were. For example, the string parser we looked at earlier needs to explicitly return a () since the return type of the function is Void? and Swift will not implicitly return for you: @inlinable public func parse(_ input: inout Substring) -> Void? { guard input.starts(with: self) else { return nil } input.removeFirst(self.count) return () }
— 12:51
Let’s convert this to the throwing version, in which we get to drop the return type and drop the return statement: @inlinable public func parse(_ input: inout Substring) throws { guard input.starts(with: self) else { throw ??? } input.removeFirst(self.count) }
— 13:05
We do have to figure out what kind of error we want to throw here. Previously we threw an empty, internal error called ParsingError just to get things moving along, and we will do the same here: @inlinable public func parse(_ input: inout Input) throws { guard self.startsWith(input) else { throw ParsingError() } input.removeFirst(self.count) } Initializer ‘init()’ is internal and cannot be referenced from an ‘@inlinable’ function
— 13:12
But now we are trying to use something internal in something that is inlinable. We add these annotations to many parsers because it helps Swift optimize deeply nested parsers.
— 13:26
It is possible to use internal types from inlinable functions, but you have to mark the internal type as @usableFromInline , including its initializer: @usableFromInline struct ParsingError: Error { @usableFromInline init() {} }
— 13:50
The parse method is now compiling, but the whole conformance is not because technically String no longer fulfills the protocol requirements: Type ‘String’ does not conform to protocol ‘Parser’
— 13:57
The way the protocol is structured now is that it has two requirements, but one requirement has a default implementation in terms of the other. That worked to get all the old parsers compiling that only implement the optional-returning requirement, but here we have a situation where we want to define only the throwing requirement and have the optional-returning requirement derived from that.
— 14:26
We can allow for this by providing a default implementation of the optional-returning parse that is defined in terms of the throwing one, but again we have to add some explicit types to help Swift out: extension Parser { … public func parse(_ input: inout Input) -> Output? { try? (self.parse(&input) as Output) } }
— 15:15
Now one weird thing about this is that it seems we have an infinite loop here. The non-throwing parse calls out to the throwing parse, which calls out to the non-throwing parser, which calls out to the throwing parse, and on and on and on.
— 15:29
We can break this infinite loop by again marking one as @_disfavoredOverload so that eventually Swift will choose a concrete implementation rather than a protocol extension: extension Parser { @_disfavoredOverload public func parse(_ input: inout Input) throws -> Output { … } … }
— 15:51
Now everything compiles, and although it took a bit of work to do, we do think the end result is a bit nicer: @inlinable public func parse(_ input: inout Input) throws { guard self.startsWith(input) else { throw ParsingError() } input.removeFirst(self.count) }
— 16:02
No return statements. Just some validation logic that, if it succeeds, we consume some input, and if it fails, we throw an error.
— 16:11
There’s a few of these we could convert, such as the End parser which verifies that there is nothing left of a collection input to parse: public struct End<Input>: Parser where Input: Collection { … @inlinable public func parse(_ input: inout Input) throws { guard input.isEmpty else { throw ParsingError() } } }
— 16:40
Or the Newline parser: public struct Newline<Input>: Parser where Input: Collection, Input.SubSequence == Input, Input.Element == UTF8.CodeUnit { … @inlinable public func parse(_ input: inout Input) throws { if input.first == .init(ascii: "\n") { input.removeFirst() } else if input.first == .init(ascii: "\r"), input.dropFirst().first == .init(ascii: "\n") { input.removeFirst(2) } else { throw ParsingError() } } }
— 17:08
Let’s look at something more interesting. The Optionally type currently returns a double optional because it converts any existing parser into one that never fails.
— 17:20
Converting this to the throwing style makes it very clear that the parser cannot fail because we just need to use try? instead of try to invoke the wrapped parser: public struct Optionally<Wrapped>: Parser where Wrapped: Parser { … @inlinable public func parse( _ input: inout Wrapped.Input ) throws -> Wrapped.Output? { try? (self.wrapped.parse(&input) as Wrapped.Output) } }
— 17:50
In fact we can even drop the throws from the parse method since we aren’t actually throwing: public func parse( _ input: inout Wrapped.Input ) -> Wrapped.Output? { … }
— 17:56
And now it’s even clearer this parser can’t fail. This compiles because non-throwing functions can satisfying throwing requirements in protocols.
— 18:07
Another example we looked at earlier was the .map operation. Under the hood it had to run the upstream parser, and then map on that optional to apply the transform. Now we just get to apply the transform directly because the upstream parser no longer returns an optional: public struct Map<Upstream, Output>: Parser where Upstream: Parser { … @inlinable @inline(__always) public func parse( _ input: inout Upstream.Input ) throws -> Output { try self.transform(self.upstream.parse(&input)) } }
— 18:37
It’s worth noting that this parse method isn’t really throwing: all it does is throw if its upstream parser throws. It’s not throwing any new errors. Ideally we would be able to use rethrows for this parser like this: public func parse( _ input: inout Upstream.Input ) rethrows -> Output { … } ‘rethrows’ function must take a throwing function argument
— 19:00
But unfortunately rethrowing functions cannot satisfy throwing requirements. There have been some discussions in the Swift forums for bringing this feature to Swift, and there’s even some experimental support in the compiler right now, but we’ll have to save that discussion for another time.
— 19:17
So, in the meantime we will just keep it throwing, but this is already much simpler and straightforward to understand what this parser is trying to do. Parser error messages
— 19:25
This is starting to look really nice. We can simplify our parsers by focusing their attention on just returning an honest output value, rather than an optional, and using throws for the failure path of the parser.
— 19:35
We could keep going converting more and more to the throwing style, but there aren’t too many more lessons to learn from doing that. Instead we’ll focus our attention on getting some error messages in place that can help us figure out what exactly went wrong. By changing our Parser protocol to have a throwing requirement we are in a better position to start showing some good error messages, but currently we are just throwing an empty error struct, and so that isn’t much different from returning nil .
— 20:03
To explore this let’s get a test case in place so that we can rapidly iterate on error messages. We’ll create a new file and get a stub in place: import Parsing import XCTest class ParsingErrorTests: XCTestCase { func testBasics() throws { } }
— 20:24
And let’s add a test that we know will fail: func testBasics() throws { var input = "World"[...] try "Hello".parse(&input) } caught error: “ParsingError()”
— 20:48
The test fails with a not very helpful error message. Let’s see if we can make it better.
— 21:00
Let’s add a message field to the ParsingError struct so that when the error is thrown we can allow the thrower to customize the message: @usableFromInline struct ParsingError: Error { let message: String @usableFromInline init(_ message: String = "🛑 NO ERROR MESSAGE PROVIDED") { self.message = message } } We’ll default it to a noisy string so that we don’t have to fix all uses of ParsingError right now and so that it will be obvious when this error has been thrown without providing a custom error message.
— 21:44
Let’s update the error thrown from the string parser to let the user know that we expected to find a particular string at the beginning of the input: @inlinable public func parse(_ input: inout Substring) throws { guard input.starts(with: self) else { throw ParsingError("expected \(self.debugDescription)") } input.removeFirst(self.count) }
— 22:06
Now when we run tests we get a slightly better error message: caught error: “ParsingError(message: “Expected "Hello"”)”
— 22:14
Though it is formatted strangely because there are some nested quotes being escaped.
— 22:19
To improve this a bit we can make the ParsingError type conform to the CustomDebugStringConvertible protocol in order to control what is printed in the error message: extension ParsingError: CustomDebugStringConvertible { @usableFromInline var debugDescription: String { self.message } }
— 22:40
Now we get a much better message: caught error: “expected “Hello””
— 22:52
So we’re making progress, but the real test of our error messaging will come once we build up much more complex parsers. Let’s work towards that by writing just a few more tests against some core parsers, such as the integer parser: func testIntParser() throws { var input = "World"[...] _ = try Int.parser().parse(&input) as Int } caught error: “🛑 NO ERROR MESSAGE PROVIDED”
— 23:24
We’re getting one of those loud, default error messages because we haven’t yet upgraded the integer parser to be throwing. Let’s do that, and let’s figure out what some good error messages could be.
— 23:39
Let’s start by making the parse on IntParser throwing instead of optional-returning: public func parse(_ input: inout Input) throws -> Output {
— 23:52
And then wherever the parse function previously return ed nil , we will throw with an error message instead. guard let first = iterator.next() else { throw ParsingError("expected integer") } … guard let n = digit(for: n) else { throw ParsingError("expected integer") } … guard !overflow else { throw ParsingError("expected integer") } … guard !overflow else { throw ParsingError("expected integer") } … guard length > (parsedSign ? 1 : 0) else { throw ParsingError("expected integer") }
— 24:15
Now things are compiling, but we still get the same bad error message: caught error: “🛑 NO ERROR MESSAGE PROVIDED”
— 24:20
This shows just how deeply integrated error message needs to be in the library. Although we have updated the integer parser to use throwing, there’s secretly another parser being used in this seemingly innocent line of code. The integer parser works on the level of collections of UTF8 code units in order to be as efficient as possible, but here we are working on just the Substring level. The way this works is by employing a FromUTF8View parser under the hood, which lets us temporarily leave the less performant Substring world for a moment in order to parse the integer, and then return once we are done.
— 25:05
This parser wraps an existing parser in order to facilitate that functionality, which in this case is the integer parser, and is accidentally throwing away any error information it propagates: @inlinable public func parse(_ input: inout Input) -> UTF8Parser.Output? { var utf8 = self.toUTF8(input) defer { input = self.fromUTF8(utf8) } return self.utf8Parser.parse(&utf8) }
— 25:09
To fix this we just need to make FromUTF8View ’s parse method throwing so that we can rethrow whatever error the integer parser threw: @inlinable public func parse(_ input: inout Input) throws -> UTF8Parser.Output { var utf8 = self.toUTF8(input) defer { input = self.fromUTF8(utf8) } return try self.utf8Parser.parse(&utf8) }
— 25:20
Now we get a better error when we run tests: caught error: “expected integer”
— 25:27
Already this is much better, because we can see that we expected to parse an integer, but there are several reasons this parser could fail. For instance if we parse a number that’s too large, it may fail to convert to an integer because it overflows, but our messaging so far just says we expected an integer. guard !overflow else { throw ParsingError("expected integer") }
— 25:43
It might be better to message to the user that we expected the integer to not overflow: guard !overflow else { throw ParsingError("expected integer not to overflow") }
— 25:50
And we can get a new test in place that exercises this logic, by trying to parse an integer larger than what fits in a UInt8 : func testIntParserOverflow() throws { var input = "256"[...] _ = try UInt8.parser().parse(&input) as UInt8 } caught error: “expected integer to not overflow”
— 25:50
Which fails with the new error message that provides a lot more context.
— 26:16
Now let’s try a parser that is composed from many parsers to see just how helpful these error messages are. Let’s go back to that users parser we defined earlier and get a test in place: func testUserParser() throws { struct User { var id: Int var name: String var isAdmin: Bool } let user = Parse(User.init) { Int.parser() "," Prefix { $0 != "," }.map(String.init) "," Bool.parser() } }
— 26:32
If we run this parser on some malformed input data, such as the “true” boolean being misspelled, we will get an error: var input = "1,Blob,tru"[...] _ = try user.parse(&input) as User caught error: “🛑 NO ERROR MESSAGE PROVIDED”
— 26:55
But currently that error isn’t helpful because somewhere in the stack of parsers that makes up the user parser we must be using something that does not yet throw good errors.
— 27:07
We can even take a look at the user parser’s type to see that it is composed of a ton of parsers: let user: Parse<Parsers.Map<Parsers.ZipOVOVO<FromUTF8View<Substring, Parsers.IntParser<Substring.UTF8View, Int>>, String, Parsers.Map<Prefix<Substring>, String>, String, OneOf<Parsers.OneOf3<Parsers.Map<String, Role>, Parsers.Map<String, Role>, Parsers.Map<String, Role>>>>, User>>
— 27:15
And we need to make sure each and every one of these throws good error messages.
— 27:21
The most obvious parser that has not yet been converted to throwing style is the boolean parser. Let’s do that real quick: public struct BoolParser<Input>: Parser where Input: Collection, Input.SubSequence == Input, Input.Element == UTF8.CodeUnit { … @inlinable public func parse(_ input: inout Input) throws -> Bool { // "true".utf8 if input.starts(with: [116, 114, 117, 101]) { input.removeFirst(4) return true // "false".utf8" } else if input.starts(with: [102, 97, 108, 115, 101]) { input.removeFirst(5) return false } throw ParsingError("expected boolean") } }
— 27:41
But alas, our error message is still poor: caught error: “🛑 NO ERROR MESSAGE PROVIDED”
— 27:50
Another parser showing up visibly in this test is the Parse entry point for opening up parser builder syntax, so let’s also update its parse method to be throwing: public struct Parse<Parsers>: Parser where Parsers: Parser { … @inlinable public func parse( _ input: inout Parsers.Input ) throws -> Parsers.Output { try self.parsers.parse(&input) } }
— 28:16
There are also a couple of initializers that under the hood make use of the .map operator, but that’s ok because we have already updated the Map parser to work with throwing.
— 28:25
But even with that converted we are still not getting any good error messages: caught error: “🛑 NO ERROR MESSAGE PROVIDED”
— 28:33
There is another parser secretly being used under the hood in our user parser, and it has to do with how parser builders work. Recall from our previous series of episodes on parser builders that we had to massively overload the buildBlock static methods that define how result builders work in order to support combining multiple parsers into one and to automatically discard the Void values that are produced along the way.
— 29:03
This is what allows us to create a user parser that makes use of 5 parsers and yet the User initializer only takes 3 arguments: let user = Parse(User.init) { Int.parser() "," Prefix { $0 != "," }.map(String.init) "," Bool.parser() }
— 29:10
This works because the two string parsers, “,”, are Void output parsers and so their values are automatically discarded and we get to focus on just the important stuff: the integer, string and boolean.
— 29:21
The way the library does this in the actual open source repo is a little different from how we covered it in episodes in past weeks. Behind the scenes the result builder buildBlock methods are constructing a variety of types known as “zip” parsers, and there’s one “zip” parser for each combination of void or non-void output up to arity 6.
— 29:39
If we jump to the Variadics.swift file you will see over 7,600 lines of generated code, with a bunch of types called things like ZipOO to represent two parsers in a builder context where we keep both outputs, or ZipVOV to represent three parsers in a builder context where we skip the first output, keep the second, and skip the third.
— 30:11
These parsers are currently code generated by a tool we ship with the library, and since this was done before we had throwing parsers all of the zip parsers using the optional-returning style. We don’t want to refactor that tool right now because that’s not really the point of parser error messages, and instead we will just hand refactor the ones we care about.
— 20:27
In particular, right now the user parser is using a ZipOVOVO parser under the hood since we want to keep the outputs of the first, third and fifth parser but discard the voids of the second and fourth parsers. So, let’s see what it takes to convert this parser to use the throwing style.
— 30:41
Currently it’s parse method looks like this: @inlinable public func parse(_ input: inout P0.Input) -> ( P0.Output, P2.Output, P4.Output )? { let original = input guard let o0 = p0.parse(&input), let _ = p1.parse(&input), let o2 = p2.parse(&input), let _ = p3.parse(&input), let o4 = p4.parse(&input) else { input = original return nil } return (o0, o2, o4) }
— 30:43
It simply runs the 5 parsers, one after the other, and if all succeed it takes the outputs of the first, third and fifth, bundles them up into a tuple, and returns that. And if one fails then we return nil .
— 30:57
To make this play nicely with throwing we will simply try each parser, one after another, and if they succeed we will return the first, third and fifth, and if one fails we will rethrow that error: @inlinable public func parse(_ input: inout P0.Input) throws -> ( P0.Output, P2.Output, P4.Output ) { let original = input do { let o0 = try p0.parse(&input) as P0.Output let _ = try p1.parse(&input) as P1.Output let o2 = try p2.parse(&input) as P2.Output let _ = try p3.parse(&input) as P3.Output let o4 = try p4.parse(&input) as P4.Output return (o0, o2, o4) } catch { input = original throw error } }
— 31:30
Due to some Swift compiler flakiness or the currently overloaded nature of the Parser protocol’s parse method, we’ve got to annotate the types, but this is only because of the trick we are doing to define default implementations for the throwing requirement in terms of the optional requirement and vice-versa. The real library will not have to worry about this, and you as a consume of the library definitely will not have to worry about it.
— 32:01
We finally have a good error message for our parser: caught error: “Expected a boolean”
— 32:14
This is already pretty nice. We are running 5 parsers on the input string, and the error message has made it much easier for us to track down that something is wrong with the boolean in the string. Next time: contextual errors
— 32:24
Now luckily the input string is quite short, and there’s only one boolean we are trying to parse, so it’s simple to figure out exactly what went wrong. However, if the input was hundreds or thousands of characters long and there were multiple places we were parsing booleans then it wouldn’t be super helpful to know that somewhere a boolean failed to parse. We’d also like to know where exactly the parse failure happened.
— 32:48
Let’s see what it takes to do that…next time! References What is @_disfavoredOverload in Swift? Federico Zanetello • Nov 10, 2020 A journey into Swift overloading thanks to this private attribute. https://www.fivestars.blog/articles/disfavoredOverload/ Downloads Sample code 0176-parser-errors-pt1 Point-Free A hub for advanced Swift programming. Brought to you by Brandon Williams and Stephen Celis . Content Become a member The Point-Free Way Beta previews Gifts Videos Collections Free clips Blog More About Us Community Slack Mastodon Twitter BlueSky GitHub Contact Us Privacy Policy © 2026 Point-Free, Inc. All rights are reserved for the videos and transcripts on this site. All other content is licensed under CC BY-NC-SA 4.0 , and the underlying source code to run this site is licensed under the MIT License .