No offense, but I don't know where that idea comes from. Systems are checking for records all the time in ways where the absence of data doesn't necessitate throwing an exception.
For instance, a page, user, or piece of media on a website may have existed at one point but was since deleted, but still has a permalink floating around the net. Is it useful, in the case that someone clicks on such a link, to throw an exception when I can instead choose to render different page content when a record wasn't found? I'll never need a stack trace for that.
> They tell you lots: you asked for something your code path wanted and your request couldn't be satisfied. Also, you've been helpfully kicked onto the alternative execution path to handle that situation.
Why would I want a code path for expected behavior? I agree for cases like a failed connection, where the system is actually broken, but there isn't anything fundamentally broken about data absence.
> Also, you've been helpfully kicked onto the alternative execution path to handle that situation.
That's not only presumptuous, but if I wanted that to happen, I can do so myself.
> Exceptions are a hell of a lot better than littering your code with null checks or error code checks, especially when you forget one and get a null pointer error.
Hmmm...
const record = store.findRecord(params.id);
if (record) {
render('show-record');
} else {
render('record-not-found');
}
or... try {
const record = store.findRecord(params.id);
render('show-record');
} catch(err) {
if (err.name === 'RECORD_NOT_FOUND') {
render('record-not-found');
} else {
throw err;
}
}
I'll let people decide which one is better. I personally prefer the first one. item = db.findItem(id)
assert(NotFoundError, item != null)
render('show-item', item)
upstream middleware: try {
res = await downstream()
} catch(e) {
if (e is NotFoundError)
render('not-found')
else
render('internal-error')
}
you can do this with other patterns for upstreaming errors like Result + Rust's handy short-circuiting, but your example doesn't demonstrate much. what about internal errors like your store throwing a database error, your first example doesn't even have a store capable of erroring? your comparison incomplete. and surely you don't do all error checking at every callsite no matter which pattern you use.Which is essentially GOTO 404
A horrible idea that I see often in clever frameworks disobeying encapsulation and reasonable control flow in favour of magic
On the one hand, it's terribly convenient when you're writing the code.
On the other hand, as clon says, it breaks encapsulation. Not necessarily, but I've rarely seen these style of Web frameworks used in a way that doesn't result in network communication concerns wormholing their way through the entire codebase. And it sometimes does do weird things with control flow. Again, not necessarily but in practice. For example, JAX-RS frameworks do the exception mapping off in some magic location that's outside the part of the call stack that you directly control, meaning that customizing the logic requires hacking some fairly complex special-purpose mechanisms into the framework.
I suspect that there's a way to achieve convenience without relying on spooky action at a distance. But one could be forgiven for looking at the way popular frameworks on popular platforms work and concluding that cleanliness must be the price of convenience.
I agree that a database API should not itself throw an error if an empty result set is found. It may throw an error if there is an SQL error, though, and really should. When one gets an empty result set where this should not happen, for instance because a row was just inserted and one is in a transaction, the user code could throw an exception instead.
Making exceptions optional is something that I have indeed done in the past because some uses of the code could benefit from exceptions while other uses did not so much.
Some things are essential to the page we are rendering, and if they are missing we should 404.
Some things are ancillary, and if they are missing we should render a stub in their place. E.g., saying that a comment is from "deleted user".
Exceptions are a good fit for the former, where we want to unify every failure. They are a poor choice for the other case, where we want to treat each failure special.
Which case a given fetch represents is a choice that should be driven by the fetcher.
They have a type, make it say MissingUserException and the special place can handle it in a special way. Ensure it is a subclass of some general exception type you also handle elsewhere and you're good.
Often people throw generic exceptions instead of using the type system.
I like it. I wish that the concept of exceptions was closer to Panic because then we wouldn't be doing nearly as much flow control.
In the Rails world (where I live) looking top down, exceptions are handled via flow control or 500 pages. Anything that doesn't result in a 500 is effectively flow control.
While I agree with Rust's semantics, it generally isn't that bad to work around and knowing which is what.
That's JavaScript, right? That's probably not the best language to use to judge the concept of exceptions.
It's cleaner in languages like Java:
try {
Record record = store.findRecord(params.id);
render(record);
} catch(RecordNotFoundException e) {
render('record-not-found');
} catch (Exception e) {
render('unexpected-error');
}My response to that would be that if it isn't a normal thing for your code to encounter a record not being found, then you should handle that state in whatever you see fit, whether that's by passing down a value or object reflecting that error state or throwing your own exception(yes, I do think such a thing can be appropriate). I still don't favor the latter, but at least with throwing your own exception your application can catch it at a higher level and will be less likely to mistake it for another exception of the same class that should be handled differently. If you are using an SDK that throws an error for a "record not found" type situation, it would certainly work in your favor if your application really does benefit from it, but I think a lot of code doesn't actually call for such a thing.
But my point is that there's no reason I can think of that an exception can't be totally optional in these circumstances. I don't think it should be assumed that everyone wants an exception. There's a reason why we use conditional syntax like if-statements because, if used try-catch syntax as control flow for everything, the flow would be bass ackwards and hard to understand. In my opinion, the example situation I described is on the borderlands where an exception might make sense but probably doesn't actually make things better.
Every project is different, and people might choose to use exceptions a lot, which is fine for them. I've personally been better off not relying on the exception-based control flow taught to me when I learned Ruby, but that doesn't mean I would come into a project and remove everyone's exception handling.
And this is the problem with error checks: They're purely optional as far as the language is concerned, so we keep getting them wrong or forgetting things, even when we're being careful.
And you are the one who knows best if something is an expected failure, so wrap that up in a fetch_one_or_none() — as a matter of fact, most DB interfaces I work with have an API of the kind.
Basically, exceptions are a decent way to keep what might need to be a separate control flow, well, separate. And you can choose at any time to "join them in". As a concept, they are quite like interrupts on the very low (hardware) level, so you can learn only one "tool" to deal with them.
(And yes, I can trivially find cases where I want errors to bubble up when a record is not found, eg. a required configuration record or similar)
The first example: - no stack trace - loosing the specifics of error to troubleshoot.
The second example: - explicitly discards the stack trace - explicitly discards the error specifics.
Hence, if this is really what you want. fine, but in a lot of cases you do want to see stack traces and the exact reasons (and meta data why an error was thrown).
> It's not a normal thing for code that needs that record that wasn't found.
This is something of a religious dispute and there are arguments both ways.
But I think it’s important to note that an exception is vastly more expensive than a simple function call or return value. So I’d say you need a really good reason to use exceptions.
I agree with the GP that in general, “lookup failed” is not an exceptional error. If it always succeeds, why are you looking up something in the first place?
I agree with the original poster that exceptions do have their place, even though they’re expensive.
If you have the primary key of a database object that you know to be correct, and you try to retrieve that record but fail, that might legitimately be an exception (unless having records deleted from under you is a common occurrence in your app).
Kotlin gets this right for the most part (although I think it would be better if it had checked exceptions).
When a new exception object is created, the slowest operation is filling information about the stack trace. It is possible to override Exception's fillInStackTrace() with an empty implementation. In this case throwing exceptions with new exception objects, is only slightly slower than using one exception object (17ms vs 15ms for 1M throws).
Deeper stacks make difference even bigger. Adding 100 nested invocations to the stack slow downs the classic approach (a new exception object created just before invocation) to 9 seconds, while alternative approaches are not affected.
That said, I agree exceptions should be reserved for exceptional circumstances. Something like checking if a record exists or not should probably use something like Optional instead, unless you are in a situation where a record "should" exist but doesn't (which is itself an exceptional case).
Maybe not Python? But Python is slow regardless.
But C++ does not provide a stack trace.
Which makes exception in C++ a very, very bad idea!
My reaction to 'the test fails because map::at threw an exception': stupid STL!!
A core dump would be so much easier to analyze.. Gdb's 'catch thow' is wonderful (well, it is after I fixed the part of our codebase which use exceptions as a control flow mechanism)
> A core dump would be so much easier to analyze..
Uncaught exceptions call terminate(), which by default calls abort(), which generates a core dump. So if you want a core dump, just avoid catching the exception.
Also works in C.
I’d simply add that most people greatly underestimate how expensive exceptions are, even in C++.
In many popular programming languages, the way to check whether some value has a certain property is with an “if” statement. It would be very odd to replace every code path inside an “if” statement with exception control flow simply because that code path “needs” some condition to be met for it it to execute.
[0] https://www2.lib.uchicago.edu/keith/ocaml-class/pattern-matc...
[1] https://dev.to/babak/exhaustive-type-checking-with-typescrip...
https://www.youtube.com/watch?v=AnZ0uTOerUI Unconditional Code by Michael Feathers
One solution to this problem is discussed toward the middle: giving data instead of asking for data avoids error conditions. Doing so pushes use of the data farther down the call tree and pulls acquisition up near the top, where we are closer to the user and thus able to more clearly decide what if anything to do about these corner cases.
That's basically limitation of the language. Null checks and result checking can basically be abstracted away with non-nullable types, option types, and result types and bind.
You only need to do match or check right at the edge, so they're not everywhere.
Like, handling a checked exception (Java) and a Google-cpp StatusOr<T> or a rust Result<T> provide extraordinary similar code. Unchecked exceptions provide a bit of extra dynamism, and languages without either are anti-user.
obj = session.query(MyObject).one_or_none() # no exception if missing
obj = session.query(MyObject).one() # exception if missing
It's not a normal thing for code that needs that record that wasn't found.
> They literally tell you nothing and there's no way to solve them without catching/rescuing them. A null value, a plain "error" object, or an error argument in a callback would have been sufficient. If I need an exception to be raised for this kind of thing, I'll do it myself.
They tell you lots: you asked for something your code path wanted and your request couldn't be satisfied. Also, you've been helpfully kicked onto the alternative execution path to handle that situation.
Exceptions are a hell of a lot better than littering your code with null checks or error code checks, especially when you forget one and get a null pointer error or your code wanders away from the root cause and fails later.