When should you conform something to Error?

here’s an API i was trying to write today:

private
func validate(
    response:ThingResponse) -> Result<Thing, HTTP.ServerResponse>
{
}

the caller uses it like this:

let thing:Thing

switch self.validate(response: try await self.request())
{
case .failure(let page):
    //  Show the user the error page.
    return page

case .success(let validated): 
    //  Continue doing our thing.
    thing = validated
}

but that wouldn’t compile, because HTTP.ServerResponse must conform to Error.

of course, anything can conform to Error, as it has no requirements, but just because something can conform to Error doesn’t mean it… should?

  • HTTP.ServerResponse isn’t necessarily an error, it could have status 200 OK
  • HTTP.ServerResponse doesn’t have “Error” anywhere in its name
  • you would never want to throw an HTTP.ServerResponse, you just want to squeeze it into a Result<T, U>.

when should you conform something to Error?

Result type design assumes that you have success and failure, it seems logical that failure part should confirm Error.

In case you need two types option, Either pattern(?) feels more appropriate. And given here your second case is ServerResponse, it makes more sense — clearly it would be odd to have it confirming to the Error protocol.

As from perspective of validate function, on the other hand, you’d expect it to return/throe some error if validation has failed. So probably it is better to have result after all, and either:

  • review what this function should return — is response here necessary?
  • if having response in errors case has sense, then you can wrap it in some kind of ValidationError that holds response inside of it.
3 Likes

Feels like it would maybe be more appropriate to have the HTTP.ServerResponse be a property on some sort of bespoke ValidationError struct? That would also allow for the inclusion of additional information about why validation failed rather than just spitting back the raw response.

Edit: heh, @vns beat me to the punch by a couple seconds :smile:

1 Like

to be clear, the HTTP.ServerResponse is the human-readable page we construct to send to the user who initiated the request to the third-party API, not the response we receive from the third-party API. the error page is non-trivial to construct, which is why it is built inside validate(response:) and not by the caller.

i don’t want to create a bespoke ValidationError struct because such a struct would be nothing but a wrapper around an HTTP.ServerResponse, and that would just introduce additional ceremony for wrapping and unwrapping the ValidationError for little perceivable benefit except to make it compatible with Result.

ValidationError also wouldn’t be very reusable elsewhere in the code base, because other kinds of requests can fail for reasons that are not necessarily “validation” errors.

I probably would go with Either type in that case, yet it still new type you might end up using only in one place — I’ve had such situation and this type actually did bother me a lot being used in one tiny place :sweat_smile: As more lightweight option you can return tuple like (Thing?, HTTP.ServerResponse?) and switch over it on the caller side. It introduces third case to handle, but assuming it should never be reached — it can be protected by assertion/precondition.

I mean, isn't 'make it compatible with result' the entire benefit you're looking for here? I agree with you that it's odd to make something which is not semantically an 'error' at the type level conform to Error, so then it feels like the only option is to wrap the HTTP.ServerResponse somehow. Even if you're not modeling it as a ValidationError strictly, it still seems like it would be potentially useful to have somewhere to stash metadata about why the response we're returning is an error—that sort of info wouldn't necessarily be appropriate on HTTP.ServerResponse since it's not necessarily an error. And even if you really just have a 'dumb' wrapper around a single HTTP.ServerResponse value, it really doesn't strike me as that much ceremony to write error.responsePage.

From my perspective at least it is more the question of what the code tells. If it is not an error from the system perspective, it is more preferable to have different types structure. While if that is an error from this view, I am totally on board of introducing wrapper type even though it might be used only there.

It is also might be debatable if server response is wrong to confirm to Error after all. I would generally go with that being odd, yet I can see cases when confirming isn’t that bad, since it might be an error and if from the system context it is a valid case — why not? It’s just that stating something as error has bigger implications on the system, and I would be more careful with it on types that is widely spread in the code.

2 Likes

making the response fit inside a Result for the sake of using Result isn’t the goal, the goal is to be able to use some sort of “early exit” pattern adapted for a web application that communicates its status over HTML.

when we are not using HTML as a communication medium, we have great patterns (e.g. try, guard let, etc.)

let x:X = try self.doX()
let y:Y = try self.doY(with: x)
let z:Z = try self.doZ(with: y)

but trying to achieve something similar with HTML feels a lot harder than it needs to be.

alas, the APIs for which it is important to observe metadata are already using bespoke error types. this is an example of an API that has no need for additional error metadata, so designing a way to allow it carry additional metadata is just eagerly adding complexity to something that doesn’t require it yet.

Yeah, if the thing you’re trying to return here is fundamentally not an error and there’s no desire to express “this is a response that is specifically an error” in the type system then sounds like Result is just the wrong tool for the job and you ought to use a different type or write your own that better models the state here. Like @vns I don’t think there’s necessarily anything wrong with conforming to Error here but it could have downstream costs of e.g. making the intent of the type less clear to someone who stumbles across the Error conformance or exposing members that are intended to be available only on ‘true’ errors. Whether that’s worse than the cost of an extra member access to unwrap the inner response… :person_shrugging:

1 Like

With the exact constraints you describe, I don't think you have a better choice than to make a struct that just wraps the page as a property. (Or don't use Result.)

But zooming out, I feel like the flow of the code should ideally be inverted, where you have a structure like

do {
    let validated = try validate(page) // `validate` could throw `ValidationError` which conforms to `ServerResponseRenderable`
} catch let error as ServerResponseRenderable {
    return error.renderServerResponse()
}

The actual catching of the error might happen in like a top-level route handler, rather than inline do/catch, but the general pattern would be to bubble the error as such, or continue as normal. This gets you the desirable linear code flow, even "with HTML".

2 Likes