Precise error typing in Swift

Thanks for bringing this up, because typed errors would have an impact on APIs if we get to the place that @beccadax pitches (and I hope we do) in [Pre-Pitch] Import access control: a modest proposal where the default behavior of import would not leak everything and you would need to use public import to import anything that you want to use part of the public APIs that you export.

When precise error types aren't encoded as part of the API, you can do something like this:

  • ModuleA.floop calls ModuleB.blip and throws anything that blip throws
  • Otherwise, ModuleB is purely an implementation detail of ModuleA; the latter doesn't use any of the former's types in its APIs, so ModuleA imports ModuleB as implementation-only
  • MainModule imports ModuleA to call floop. It can either:
    • Import ModuleB directly if it wants to handle those errors specifically
    • Ignore ModuleB and treat any of its errors as existentials with generic handling

The benefit of implementation-only imports is that dependency graphs can be pruned, which is very important for build performance of large projects. If MainModule doesn't care about the specific types of errors that might come from ModuleB and just wants to handle them generically, it doesn't have to include ModuleB (and its transitive closure) as a compile-time dependency. Or it can choose to explicitly opt-in to using ModuleB by depending on it and importing it.

If APIs started adopting typed errors everywhere, then those error types would have to become part of the public API of the module. In the example above, if ModuleA.floop was declared as throws(ModuleB.BlipError) then it would have to import ModuleB as a public import instead of an implementation-only import, thereby "infecting" ModuleA with the requirement that ModuleB also be present in the dependency closure passed to the compiler whenever anyone imports it. MainModule would have no choice but to ensure that ModuleB was present, whether it actually used it or not.

This can be subtle, but discouraging people from naïvely leaking implementation details into their API when they're really not necessary is critical if we want to move toward a model where public imports are no longer the default.

So I agree with much of the sentiment that I've seen so far in this thread: typed errors have their place in certain situations, but the default should be to not use them and most users should be strongly discouraged from doing so. I'm not sure what the best way for the language/compiler to do that is, though.

It almost feels like there should be a higher barrier to declaring that you throw errors declared in a different module than errors declared in your own. In a future public vs. implementation-only import world, that barrier could be having to change an import to a public import (assuming the module wasn't already public imported). And maybe that would be enough to force an author to think about that API boundary and its implications.

6 Likes