6 ms·
The idea that there is only one way to propagate errors, namely "up" in the call stack, is totally misguided. IMHO. It leads to function call dependencies, and
by jstimpfle 4y ago
The idea that there is only one way to propagate errors, namely "up" in the call stack, is totally misguided. IMHO. It leads to function call dependencies, and ultimately to the kind of code where we're thinking to much about the code and too little about the data structures.
There is an alternative to returning error codes or throwing exceptions: Put the errors on some context handle - e.g. set an error flag. This taints the context where the error occurred, and if you do it right you can contain the error and replace / restart / shutdown that context in isolation.
And importantly, it allows the client to do the error handling at a more central point.
This kind of design can require that you do a little "paranoid" error checking at several entry points in the implementation of your API, but it's well worth it given that you save a lot of immediate error checking on the client side. Also, if you structure things right you can minimize the need for error checking in the implementation itself.
A simple and well-known example of this approach in C would be the FILE API from stdio.h (fopen()/fread()/ferror()).
This and returning NULL/error codes is how I structure my applications. I have a strong distaste for exceptions, for the same reasons mentioned in the other post citing Joel Spolsky. But exceptions are fine for quick and dirty scripts. And some people maintain that you should structure whole applications purely with RAII and rely on exceptions to cleanup your contexts perfectly and immediately. I'm not convinced that this is practical, but honestly as probably most, I'm content just living in my own bubble...
- badpun 4y ago> There is an alternative to returning error codes or throwing exceptions: Put the errors on some context handle - e.g. set an error flag. Ah, the OpenGL way. I don't think there are many fans of that, except for perhaps low-level/perf-critical code.
- jstimpfle 4y agoYup, it's also basically a requirement for "asynchronous" APIs. Or the other way around, returning immediate error codes requires synchronization, and therefore increases coupling. I don't care how many fans there are - when I can reduce error checking clutter by a factor of 5, and do error handling where/when it best fits, I know it was the right approach.
- badpun 4y agoDepends on the language I guess. Asynchronous APIs in Scala for example use way nicer ways of handling errors (they essentially return something like a Future of a type than can be either the return value or hold the details about the error that occured).
- jstimpfle 4y agoWhat can I say, a future is exactly the kind of handle I described, isn't it? The difference being that these async frameworks do too much work in composing all these futures IMO. I prefer simple explicit event handling. Async runtimes probably make sense for web request handlers though.
- badpun 4y agoWith monadic futures, you can elegantly compose them and basically have great error handling while barely writing a line of code. In your solution, I foresee codebase where 50% of the code are ifs which check the state of the context.
- jstimpfle 4y agoWell I described an approach how to structure code to avoid checking object states (error handling) most of the time. For example, when you use a FILE like handle to write to a file stream or similar, you don't have to check each time you call fwrite(). It's the opposite, it's sufficient to check at the very end (if you're interested at all). The way I've been writing code for the last 2 years or so, there is almost no error handling at all. I check buffer ranges whenever I'm writing directly to memory using a pointer + capacity / size pair. I could probably avoid that as well with some C++ magic but even that is rare enough that I don't even bother. Most of the other error checks are in the application setup / teardown code in the hull of the application - typically one check per codebase per error or object type. Nothing to worry about, I would have to do just as much work with RAII or any other mechanism. The easiest way to handle an error, for a statically set up object, is to attach it to the "process object" i.e. exit() immediately. The next easiest way is to attach the error to some subordinate state object, where the appropriate subsystem can later pick the error up and reset the object or take appropriate action. I've already had my phase of wasted months and years composing monads and frankly I don't want to get back there but would rather get real work done. In practice, working with monads amounts to a lot of headaches how to wrap, unwrap, and lift things that I could have simply written as straightline code in a non-fancy language, after spending some reasonable thought when and what things should actually best happen. If you need complicated type hackery to automate writing error handling code, what you are doing is... too complicated.
- simiones 4y agoError states are the worst of all worlds, and an excellent way of hiding bugs in rarely exercised code paths. They simply don't work, at all, in any kind of concurrent application, even a single-threaded one. That way lies "Error: Operation completed successfully" hell. Please don't take this advice.
- jstimpfle 4y agoNow tell me what exactly was wrong with fopen()/fread()/ferror()? > They simply don't work, at all, in any kind of concurrent application, even a single-threaded one. What am I missing here? Why does this make no sense at all to me? For concurrent programming, containing errors / handling them asynchronously is basically a requirement. It's how distributed systems work.
- simiones 4y agofopen()/fread()/ferror() work decently well only because fread() is an inherently synchronous operation. Still, returning the error from fread() would be infinitely better, except that C doesn't support multiple return values in any nice to use form. But, if it would have made sense to call fread() on the same file handle concurrently, you would have to add synchronization logic (externally or internally) before checking the error flag, and even then, there would be no way to signal two different errors occurring in two different concurrent fread() calls - one error would always overwrite the other. > For concurrent programming, containing errors / handling them asynchronously is basically a requirement. Sure, the concurrency implementation ultimately has to somehow store errors, but this doesn't have to be exposed to the client API. For example, C#'s async/await handle exceptions like you would expect: an async function can throw an exception, and await will throw the exception to the caller of that await (or send an AggregateException if this is caused by something like AwaitAny/AwaitAll). The same thing can be done with error results. Alternatively, in CSP style, error values can just be regular events that a process may continue from (somewhat equivalent to sending an error value on a `chan` in Go). As I said, in all of these cases, the runtime does have to do what you are suggesting, as you would if you were writing interrupt handlers in an embedded program. But I don't think there is any good reason to casually do this in normal code, as it adds significant synchronization requirements to what may otherwise be/seem single-threaded code. For example, imagine if malloc() had a corresponding merror() like fread() to indicate why an allocation failed. If you called malloc() and received a null pointer, you would probably see a "no error" state before you got a chance to check the result of merror() in a concurrent program, since there's a good chance something else had successfully allocated memory before you got a chance to read an error. To avoid this, you would have to explicitly guard with a mutex from before malloc() to after merror(), instead of relying on the internal locks in malloc() (and thus also lose all of the optimization opportunities for thread-local allocation).