I found myself writing this awful code, and I would like to know if there is a better way in current Swift?
public convenience init(name: String) throws(LoadOrCompileError) {
let vertex, fragment: String
do {
vertex = .init(cString: try Assets.load("\(name).vs"))
fragment = .init(cString: try Assets.load("\(name).fs"))
} catch {
throw .init(wrapping: error)
}
do {
try self.init(vertex: vertex, fragment: fragment)
} catch {
throw .init(wrapping: error)
}
}
Also, why can't it just be:
public convenience init(name: String) throws(LoadOrCompileError) {
do {
try self.init(
vertex: .init(cString: try Assets.load("\(name).vs"))
fragment: .init(cString: try Assets.load("\(name).fs"))
)
} catch let error: Assets.LoadError {
throw .init(wrapping: error)
} catch let error: CompileError {
throw .init(wrapping: error)
}
}
Or even better, just specify multiple errors separated by commas to avoid this boilerplate enum altogether? I don't believe this would be much more difficult to resolve than function overloading.
Maybe the intended way is to use protocols, however in Embedded Swift that is not a thing. It's also needless overhead. What am I supposed to do here?
It's also very unintuitive, it's taken me a moment to even remember I can initialize variables late as without this feature it would be inexpressible.
Declaring multiple typed errors was specifically decided against in the typed throws proposal, so as to avoid the Java exception type explosion problem.
According to the proposal, exhaustive checking of multiple error types inside the do is unsupported due to worries about compile and runtime performance. Since you have a direct init already, you can just move the logic in there. Really I would just make all of these failures part of the same error type, rather than continually nesting them, as they're at least somewhat related.
Absolutely not, these are completely distinct. The Shader init takes the shader source code and throws errors related to shader compilation, whereas Assets.load takes a path relative to the bundled asset directory and throws IO errors such as not finding an asset.
My signature is correct, it is a convenience initializer which takes a shader name and loads both sources before letting the existing initializer do the usual work.
I don't see how IO and compilation errors are even remotely related. Am I supposed to add OpenGL errors to a different, platform independent module which handles IO?
Also, why would I make the primary init aware of IO errors, as it performs no IO at all?
That is really sad. I'll have to read that proposal then, as I struggle to imagine an example of such an issue that doesn't originate from bad design.
Making lots of errors into any doesn't remove that complexity only hide it
You're literally writing a function that can produce both errors, so apparently they're at least somewhat related. But it was just a suggestion. Obviously if you're multiplatform and multimodule, the design considerations are different. The intended solution here is to simply stop throwing typed errors. I don't really agree with their logic here, but you can read the suggested use cases here. And you can read the logic behind not throwing multiple error types here.
This must be from before Embedded Swift since "one should use any Error" completely disregards the fact that existentials don't exist in this mode :(
Losing the static types is one thing, but it's not something I could do even if I wanted to.
The only choices in Embedded Swift are to panic or, even worse than "exception type explosion", exception type explosion except I need to manually write the enums
Wait, in embedded mode one cannot use the any Error type? That is, you cannot simply write a function that throws without specifying the thrown error type?
No, at least last time I tried to use itany was absent in embedded mode, and so were all the other features which rely on runtime type information.
I use embedded mode in order to cross compile games to wasm, and in the future when the standard library can actually be built without the entire toolchain, for fun platforms like the sony psp
At the moment I stopped cross compiling, but I don't want to accidentally start depending on non embedded features — it would require potentially lots of work to remove from all the places such an API is used when I do eventually want to port my code.
I'm aware that existentials are currently not allowed in embedded mode, but I thought that Error, being a special protocol already in non-embedded mode (for example, any Error conforms to Error) would also be somehow special in embedded.
If that's not the case, I would consider 2 options:
either there are already plans to support any Error specifically in embedded mode, with a reliable roadmap, or...
I would not write throwing functions at all, because the Swift model for errors doesn't support error unions, and the do-catch dance to wrap errors into other errors into other errors is rather bad.
To be clear, I think Swift error model is excellent, and I would also strongly advise to use any Error, but if that can't be used in embedded mode then throw(Something) + do-catch dance seems a much worse option compared to using Result instead.
What I mean specifically is that I think defining static functions that return Result<Self, SomeSpecificError> instead of init() throws allows to work with Results that, being simple values, can be manipulated easily.
For example, your initial code could be written as:
static func from(name: String) -> Result<Self, Either<Assets.LoadError, CompileError>> {
Result<(), Either<Assets.LoadError, CompileError>>.success(()) // this is here to just start the chain
.flatMap {
Result {
String(cString: try Assets.load("\(name).vs"))
}
}
.flatMap { vertex in
Result {
String(cString: try Assets.load("\(name).vs"))
}
.map { fragment in (vertex, fragment) }
}
.mapError(Either.left)
.flatMap { vertex, fragment in
Self.from(vertex: vertex, fragment: fragment) // this also returns a Result
.mapError(Either.right)
}
}
This is pretty verbose, and doesn't look much better than your initial example, but once a few patterns emerge, it becomes possible to define generic manipulations on Result that would eventually make the code easier to understand.
For example, off the top of my head, with a simple zip function one could write something like this:
static func from(name: String) -> Result<Self, Either<Assets.LoadError, CompileError>> {
zip(
Result { String(cString: try Assets.load("\(name).vs")) },
Result { String(cString: try Assets.load("\(name).vs")) }
)
.mapError(Either.left)
.flatMap { vertex, fragment in
Self.from(vertex: vertex, fragment: fragment) // this also returns a Result
.mapError(Either.right)
}
}
Also, the Either type could be split in multiple Either2, Either3, Either4... that could also be combined generically, in case of more than 2 error types, or nested eithers.
I would agree to some extent, at least Swift does have typed error handling, many languages seem to omit this nowadays for convenience of lazy programming.
If a function genuinely does require specifying say 10 different error types writing it as any Error saves work only for the lazy programmer — and their code is probably just forwarding the error, no better than try!. Someone who exhaustively handles errors on the other hand now has to check the implementation to even know what to handle, and think really hard when making changes to make sure this doesn't go out of sync.
I don't understand the generally negative attitude to actually carefully handling all the errors.
Having to type out 10 errors to forward them should make someone think twice if their function really should be throwing all of them in the first place But I guess it's too late now.
I recommend you to read `typed throw' proposal review and related topics, there are great explanations why it is a bas idea in common to throw lots of error types. Like this one:
But your case is something special. While you can not use any Error existential, you can make your own type-erasure container:
struct AnyError: Error {
init(error: some Error) {}
}
I also don't know if this idea is suitable for your needs, but may it is possible to use something like CommonError or BasicError.
Then Assets.load and self.init(vertex: vertex, fragment: fragment) will throw this BasicError and you don't ned to wrap different error Types.
Th overall idea is to answer the question 'do you really need so much different Error types for each function / class / task?'
One example is Keychain. There are very much errors can happen working with keychain, but really we need to think about them like `several important errors and the others'. Exhaustive handling of all the keychain errors would be a nightmare.
These arguments are incomplete. Something like "it would break callers as soon as you need to add a third possibility" makes the assumption that it somehow isn't what I want That is exactly what the choice between an explicit list of errors and any Error would mean.
How would it be different from enum exhaustiveness? If I wanted code not to break I'd simply do something along the lines of catch let _: some Error where I log or panic. In fact it would allow me to omit the catch-all when my handling is exhaustive, reducing boilerplate for the caller.
I already mentioned that (simplifying types a bit) (FileName) throws(FileError, CompileError) -> Shader Is the natural signature for the composition of (FileName) throws(FileError) -> ShaderSource and (ShaderSource) throws(CompileError) -> Shader
With how typed throws got implemented such composition is inexpressible.
For example, if I had some previous code that used these separately and wanted to migrate to the sort of composed convenience function, it now can't just replace the three try uses with one, but rather be changed to unwrap the enum or change its own signature.
And having the choice for an api to be static does not mean all apis are suddenly obligated to do so.
Available in Embedded Swift or not, this is fundamentally needless overhead to box what is in many cases just enums.
Perhaps I could do something with Result and variadic generics, however the latter also does not work in embedded mode.
I disagree, and I don't think using any Error makes a developer "lazy".
In general, I think that it would be desirable to have the following features:
union types, even if they're only used for errors, for example if I declare throws(A | B) then I can throw either A or B and it's going to be automatically lifted to A | B;
being able to write throws(some Error) so the developer wouldn't see the actual error type, but the compiler would be aware of it (like it's done with opaque return types in general), for example to provide exhaustive error catching;
this is crucial, the idea of needing to rewrite dozens of function signatures because of a minor change somewhere in the chain is terrible, even if it could be desirable and have some application is some limited and constrained scope.
But of course, give limited resources, the teams that work on the language must prioritize, and I'd say that there are things that are massively more important to work on, including in the embedded space. In fact, I think the 2 desirable things I mentioned above should be very low priority, in-case-there's-time things.
I used to think that being able to exhaustively catch on errors was a very important thing, but I stopped thinking that a long time ago. Having exhaustive checks on errors is as important as having it on all other types only if errors carry business logic meaning, but if their only purpose is to carry around pieces of information that are simply going to logged somewhere or shown to users, then exhaustiveness doesn't matter, what matters is that the right abstractions are in place to carry that information.
Should errors carry business logic meaning, then? They should not, otherwise there would be no point in distinguishing errors from regular models. Suppose that a function has this signature:
where SomeValueEnum has several cases related to specific business logic scenarios, and SomeErrorEnum also has some of these cases, but other cases would just be logged somewhere. I think the former cases should instead enter in SomeValueEnum, otherwise the boundary between what's an "error" and what's not an "error" would stop making sense.
In my view, an "error" is something unexpected and undesirable, so it doesn't make sense to exhaustively catch over it: in some layer it could still be possible to catch one or more specific error types to do a specific thing with them, and just short circuit the others (a classic example is specific HTTP error codes, that are used to carry business logic).
Applying these principles has worked very well for us, to the point that even if the 2 desirable features that I listed at the beginning were present in Swift already, there would probably still be no place in the app with exhaustive errors checks. When we started looking at it, it was pretty easy to realize that the amount of work that was done to be able to carry full error type information was completely disproportionate to the actual utility of it.
I think this is what we disagree on, I strongly believe in the Zig approach — error handling is always part of the business logic, and personally I would even like it to be as pedantic as in Zig, where even allocation returns an error union.
What if my game allocates too much terrain at once? I think it must be part of the design to catch this and in this case offload some older cached terrain to disk. Crashing here is unacceptable.
A shader doesn't compile? The UI should communicate this to the user and give them the choice to run with potentially broken graphical effects. Or maybe fall back to a more compatible shader.
For any exceptional situation there are usually unique ways to ensure everything remains robust, and concrete error information, and Swift/Zig verbose try syntax help visualize the flow of errors through the design.
There isn't actually any conflict here though.
I don't think I should be forced to use less strict errors just because you have the opposite opinion, and similarly, I don't think you should be forced to use strict errors just because I don't like them.
What I do think is that this choice is the responsibility of a project, not the programming language.
Surely. Let's make it clear: do you want to propose language changes (like union types) or to solve a concrete task right now? I don't insist on what I've said, just suggesting ideas.
In case of the second there is nothing in embedded mode except Either /OneOf3 / ... for error composition and AnyError type-erasure container instead of any Error existential.
You can also use Result type, but it is not more expressive than typed throws and also have a downside – you can't just use try and throwable composition, though it still rather useful. In my projects there are helper functions to convert throwable function calls into Result and vise versa, also functions to process multiple Results, e.g. func combinedSuccessOrFirstFailure(results a: Result<A, E>, _ b: Result<B, E>, _ c: Result<C, E>) -> Result<(A, B, C), E> with different arity.
Definitely, and I'd say that if I was in your specific situation (given my, admittedly, very limited understanding of it) I would not use Error to represent the scenarios you mentioned, if the potential "erroneous" outcomes carry business logic meaning.
For example, in this hypothetical
I 100% agree that crashing is unacceptable, and given the fact that a specific decision must be made if either the game allocates too much terrain at once, or it doesn't, I would represent the outcome with a specific return type, not an error, for example (sorry if the code looks silly):
enum TerrainAllocationOutcome {
case aboutRight(Terrain)
case tooMuch(New, Old)
}
func allocateTerrain() -> TerrainAllocationOutcome { ... }
But the allocateTerrain might also actually throw, because something else, unexpected and undesirable, could happen, related to its internals, that can only be logged and recovered by trying again. If this was the case, then one could be tempted to also represent the tooMuch with an Error, but that wouldn't be great design to me, because it would mix up in the Error case things that require specific business logic and things that are only logged and retried, making the Success/Error boundary not meaningful.
On a side note, there is also a similar problem when implementing web APIs that give some HTTP error codes some business logic significance, mixing up actual errors with things that are not errors at all, making modeling harder and removing much of the utility of remote logging systems.
That's also why I suggested using Result, that allows to fully model a function outcome with types, and can be manipulated with generic functions.
To be clear, I agree with the need to be pedantic with the outcomes of functions, and I agree that the error handling model could be more powerful without sacrificing usability for people that use errors in a different way. I'm just saying that the current error model implemented in Swift doesn't support very well approaches where errors have business logic significance, and need precise handling at various levels of a call stack, so alternative approaches must be sought (at least for now), like a different way of modeling "error outcomes".
In Java this would be the difference between normal exceptions and runtime exceptions. I don't think internals should be exposed in just normal errors.
To me, errors are still expected, maybe things the program has no control over such as a file not being found.
For example, in Java, trying to load a native C function that doesn't exist will throw a NoSuchElementException which is a subclass of RuntimeException. It can still be handled if expected, maybe you fall back to an older function. But if the issue is the program accidentally using a class on the wrong platform, and the internal implementation of the class can't handle it, it probably shouldn't be handled in general as it points to a serious bug. In this case some erroneous logic in the program itself
What you're describing is treating Swift errors as the equivalent of the runtime exceptions, but I think they're too strict for that.
I feel like Swift could provide a version of Java's runtime exceptions but without just making them invisible and needing the runtime part
Swift exceptions
It could be a new special protocol, like Error, but called something else — perhaps simply Exception (but this could be confusing). It would have slightly different semantics
I think this could even avoid being ABI breaking and stay opt-in — instead of being inferred and propagating everywhere, or needing a runtime, the behavior mirrors existing Swift, and by default panics. It could be handled like this:
do {
return except MyClass() // declare we are handling the implicit class allocation error of every class `init`
} catch let e: AllocationException {
// Handle the exception here
}
With similar variants to try, like except?, but the default is already implicitly except!
They could still be forwarded if one chooses, by explicitly adding the exceptions to throws(...) specification, or maybe a separate except(...)
Using except! shouldn't break ABI, think of it as functions that can throw exceptions having two implementations, one that actually returns exceptions and another which crashes in itself. The panicking would happen inside the implementation.
Using except without do-catch would implicitly add the exception specification, forwarding any exceptions. This is fine because if the user of our API doesn't use except themselves, they just get the old panicking implementation.
For example here is a (simplified) improved precondition function:
If called without except the signature essentially loses the except specification, so stays unchanged.
I think Swift, more importantly Embedded Swift definitely needs a way to deal with especially allocation errors.
Despite not being exceptions in implementation, they would just be Swift errors with alternative semantics, I like the name Exception as it correctly suggests something being a truly exceptional situation.
This probably doesn't make sense for "Using Swift" anymore though
I think in Swift the distinction is different. Given the current Swift model, I'd say that these are all "errors", but some are "fatal", and as such unrecoverable and crashing, some are "non-fatal", thus recoverable, but still unexpected.
To be clear, by "unexpected" I don't mean that the developer thinks that they will never happen, but that they have no business logic significance.
For example, consider the FileNotFound error that you mentioned. Say that at a certain layer you're using a file manager that could throw several errors, including a FileNotFound error, and let's say that the FileNotFound has business logic significance (that is, in response to that error I'm going to do something very specific) but the others don't.
The way I would approach this is by putting a layer between the file manager and its potential clients, that will transform the FileNotFound error into a value that is not an error, moving all business logic outside the error space.
This plays well with the current model, in which I can only declare that a specific error type is thrown, and there's no automatic error union + throw(some Error), because in this model only low level functions will declare a specific thrown error, and in case some of those errors have business logic significance, there's always going to be a layer that transforms those errors into non-errors.
To add to that, the main way I use errors in my Swift code is to provide a human-understandable description of what’s gone wrong. An excellent example of this is DiagnosticsError used when writing macros. The any Error existential is particularly valuable here because a complex operation could fail at many points, but ultimately my handling for those is the same: extract a localized string message and display a message to the user saying “Sorry, [thing] failed: [message].”
If you’re targeting an OS with a virtual memory system, a call to malloc() (or whatever similar mechanism Swift allocation uses) will almost never fail because of the ability to go into swap. Instead, you’ll just get performance degradation from having to page data in and out of RAM. So malloc failing is really a signal that your process is entirely out of control and probably violating some of your other assumptions. If you’re storing bulk terrain data, you could try putting it in a memory-mapped file to allow the OS to intelligently page it in and out of memory as needed, or use explicit checks of available memory to clear caches.
That definitely doesn’t apply as well on embedded systems, but I’d argue that making memory allocation failable isn’t a good solution there either because it introduces large amounts of additional control flow into your code that is nearly impossible to test. If out-of-memory errors are causing issues for me in this situation, I’d approach it by extending the memory allocator to detect a low-memory condition and call out to application-specific code to free up some memory while there’s still enough free space left to do this work.