19 ms·
Exceptions don't deal with something, they cancel everything else that was scheduled to run after. If you catch an exception immediately (in the calling functi
by jstimpfle 4y ago
Exceptions don't deal with something, they cancel everything else that was scheduled to run after.
If you catch an exception immediately (in the calling function) then that's immediate handling. But then there wasn't really a point in using an exception vs returning an error code.
Exceptions mainly allow you to be lazy, and work well with systems that have some introspection how to cleanup themselves: Garbage collection, RAII, OS processes...
I'm not convinced that relying on these systems scales well to complex applications. And maybe I'm doing something wrong, but I don't think manual error handling is a big hassle if you put some thought in structuring the system.
- simiones 4y agoMy question was "what does it mean to to you (or to Joel Spolsky, since it's his words originally)" to "deal with an error immediately"? Do you see some third option in addition to my 1 and 2? Otherwise, whether you write: { auto x = new int[100]; int err = foo(x); if(err) { delete[] x; return err; } //do something with x delete[] x; return 0; } or you write { auto x = std::unique_ptr<int>(new int[100]); foo(x); //do something with x } I don't see any significant difference. The exception case is shorter (less code to review), while the manual error return code is more explicit (easier to review the code).
- jstimpfle 4y ago> I don't see any significant difference. Because this is a toy example. In a real system, you avoid allocating / deallocating the buffers right before and after you work on them, to begin with.
- simiones 4y agoYou are not answering my question. The fact that x is a buffer is irrelevant. Here, I'll rephrase: Otherwise, whether you write: { auto x = fopen("someFile"); int err = foo(x); if(err) { fclose(x); return err; } //do something with x fclose(x); return 0; } or you write { auto x = std::ifstream("someFile.txt"); foo(x); //do something with x } What is the fundamental difference?
- jstimpfle 4y agoI'm not sure what obvious or not-so-obvious observation you want me to state, but in my point of view there is no difference relevant to this discussion; both is uninteresting and bad code that you want to avoid writing in a larger system. It doesn't change much whether this is a buffer or any other kind of resource. As your system grows more structure, the handling of resources (including errors) is moving more and more towards centralized subsystems. You want to avoid complicated fallible and _repetitive_ logic inside random "application" code. In application code, you don't want to just open files, incurring all sorts of non-deterministic latencies, blocking the current thread of execution. What you do instead is, you move to an event based system, where your "application modules" are completely contained in a shell of "platform code". Note that both your examples contain really boring _repetitive_ code, whether visibly in the source, or only in the assembly generated by RAII mechanism. This is _bad_ as soon as you have multiple of these chunks of code. Think web applications. Do you accept() a socket from within any random request handler? Does any request handler read() from the socket to parse the request data? No, all that happens in a driver that just notifies the individual request handlers when more work is coming. Consequently, the only places where errors from accept() or read() need to be handled are at central locations within the framework. This requires some structure, for example your web request handler has to be registered with the web framework, like framework.register_handle("POST", "/foo", handler). Or similar, I'm actually not a web person. In the same way, you don't fopen() files in any random place in a 100 KLOC codebase. Instead you have abstractions for asset loading, media playback, savefile storage, ... whatever and _ideally_ all those come back to probably some kind of caching layer, maybe an async I/O worker pool, and finally a single line of fopen() (or open()) in your entire codebase. Note that for this to work, you have to change your general approach to things. You can't expect to write script-like code anymore that does "save file now" or similar. What you do instead is, you push a job to some subsystem, and when the job is done you will be notified later so you can complete the necessary tasks. It's all event handling.
- ryandrake 4y agoMy philosophy has always been: Clearly separate the input from the result from the error, using different features of the language for each. Pass input variables only as arguments, return only the "result" of the function, and throw errors. Take the classic (and simplistic) divide function, in pseudocode. There are a number of ways you can do it, and I'm not listing them all: divide(numerator, denominator) returns quotient, throws error divide(in numerator, in denominator, out quotient) throws error divide(in numerator, in denominator, out quotient) returns error divide(in numerator, in denominator, out quotient, out error) divide(numerator, denominator) returns tuple(quotient, error) I've always considered the first one more expressive, more usable by the caller (e.g. you can pass the result of the function directly to the next function), and the least confusing. The second one is -meh-.. I'm just not a big fan of input and output arguments. You have a return value, why not use it? The last three are particularly bad, because the caller must remember to check the error value manually (unless your compiler can remind you).
- throwawaymaths 4y ago> Exceptions mainly allow you to be lazy This is not quite right. Exceptions allow you to be safe. Think of it like a factory that's practicing Toyota design -- any worker can pull a alarm that stops the entire production line if there's a critical safety issue. For code, this alarm pull is the exception. And when you pull it, everything restarts from scratch so it must be in a good state on recovery. Sometimes if a component fails, there is no clear and logical way to recover from the intermediate state (perhaps there are unsafe or insecure assumptions that would need to be made to keep going), so it's better to give up.
- jstimpfle 4y agoI agree about the idea of pulling the alarm handle when some state goes wrong. However the problem is the notion that the problem is in the code (after all, exceptions cancel the scheduled code). No, the problem is in an individual object / data structure, and as I said simply cancelling all the stuff is fine for a script (or a web request handler if you want) but I don't think that's how you build complex, reliable and deterministically performing systems.
- throwawaymaths 4y agoThe claim is not that the problem is in the code. The problem is something like "database is unreachable" and yes, this is exactly how you build complex and extremely reliable systems -- it's the underpinning of how Erlang systems can have uptimes in the years. Fundamentally it's also how kuberbetes works. One of the containers has pulled it's alarm, so the pod "supervisor" restarts it.
- jstimpfle 4y ago> The claim is not that the problem is in the code. But exceptions act very much like the problem was the code. They cancel the following code in any function on the stack, which in general is unrelated to the error in the state where the exception originated. Another problem is that the exception originates from code instead of the object that is in a bad state. In other words, you need to step on a landmine to even notice that the object was already bad. It would make much much more sense if the "exception" was stored with the object/handle instead of propagated up an unrelated call stack, a single time. With Erlang exceptions, the model might fit better I don't know. I looked at it many years ago but never used it.