Sometimes, when we try to improve an approach to work well for even more use cases, we lose track of the first goal and end up becoming the very thing we swore to destroy. It seems like this has happened in your company.
This requires some autopsy, so I'll address points from your question in order:
Then, the object should be constructed anyway and there should be another way to check if the object is valid, like an isValid()
method.
It's subjective whether that is the right approach or not. Not necessarily a problem; but I do want to point out that a significant number of people disagree with your approach so it's not as clear cut as your company is making it out to be.
This is one part business context, one part subjective coding style.
- When something throw, at one point in the call stack this should be handled (using try...catch).
Ideally, yes, exceptions should be caught. The application blowing up due to a foreseeable mistake is not good quality.
Foreshadowing: notice the stressed part in the above.
- If a throw is not caught, stack unwinding will go all the way to the top, at which point it will encounter a global catch block that will do something like this: display the message "Something went horribly wrong", try to generate a crash report, and shut down the app.
I would put an asterisk on having a global catch-all that catches everything without even considering if it can do anything with it. It's contextual.
If there is a security concern, such as not leaking implementation information to an untrusted consumer (which is often the case for web-based APIs), yeah a last line of defense that catches any exceptions that bubble to the top, logs them, and returns an uninformative 500 Something went wrong
to the requester is perfectly okay.
However, this does not seem to be your scenario, as you're (a) dealing with a desktop app and more importantly (b) are intending to still crash your app and are just logging details before you do so.
Be very careful about this catch-all making your developers lazy in not bothering to see if specific exceptions can be handled more gracefully on another level. The existence of a last line of defense should not lead to you ignoring all other defences.
I suspect that your company may have run afoul of the above advice, and their "no exceptions" rule is in response to developers never really considering if there's an appropriate catch for thrown exceptions. It just sounds like it.
- We cannot be sure that future programmers will correctly use your object and handle correctly all edge cases. If someone uses my object, forgets to add catch, and passes through testing, we are at risk of having a "Something went horribly wrong" message in production, and this should be avoided at all cost.
This is where it goes severely off the rails. At its very core, the issue here is the expectation that an implementation is aware of and responsible for any and all of its consumers.
The designer of the throw should not be aware of who is using their design, nor if and how they choose to handle a raised exception. That's the entire point of an exception: the consumer gets to opt in to handling specific exception or otherwise letting them bubble up further (in cases where they can't meaningfully handle it anyway).
Dictating that the design of the class should be different because you can't universally prevent that the consumer might do something wrong with it is asinine. This is arguing that we shouldn't have forks because someone might stab themselves with it. Not only is that helicopter management, it also misses the point that stabbing things is the explicit purpose of a fork.
Stepping out of the analogy, giving your consumer the ability to choose to handle the exception or not is inherently part of the purpose of an exception in the first place. Anyone who suggests that potentially not catching an exception is the reason to disallow throwing them does not understand exceptions.
Instead, we have a lot of if (myobj.isValid()
) everywhere, and empty constructors alongside with init(....)
methods, which I have a feeling are a bad pattern.
As unforgiving as I was in the previous paragraph, I'm going to call for nuance here. While throwing exceptions can be a very useful practice, it should not be done for expected outcomes. Exceptions are, by their very definitions, expressing an exceptional scenario.
With regards to object construction, the question is who is constructing the object. For example, if you are deserializing JSON from an external source, then receiving bad data (i.e. structurally sound but not passing an arbitrary validation rule) is well within reasonable expectations and therefore throwing an exception would not be a great way to handle this.
On the other hand, if this class is only being instantiated by your own private domain logic, then any bad values being passed into it are a sign of a significant flaw in your domain logic, which is something you want to have fail loudly, because if your domain logic is not doing what you think it does, that can have significant impact on your application's behavior.
Anyone who tries to tell you that the approach should be a universal one without even investigating the context, is not a good source of information and advice.
What would be a better approach, that allows programmers to throw when needed, and ensure that we never trigger the ultimate "Something went wrong" fail-safe?
As established above, I disagree with the premise that we should always have perfect coverage in conscious catching of specific exceptions; by the very nature of what an exception is.
As established slightly further above, it can make sense to at the same time have a catch-all last line of defense in cases where you'd otherwise be leaking sensitive information to people who should not be privy to exception details.
A more healthy flow would be something along the lines of:
- Design phase - do we throw an exception?
- Is the bad path a "normal" outcome or is it describing something unreasonable?
- Can we continue with the current situation or has it become impossible to deliver what the consumer expects?
- Consumer phase - do we handle any exceptions?
- Which exceptions are being thrown?
- Can we meaningfully respond to (some of) these scenarios and handle them better than letting them bubble up?
- Security phase - do we need to stop the bubbling up?
- Is the eventual recipient of the exception information a trusted actor, or should we block this information from reaching them?
After all this has been considered, if there's an exception that bubbles up and crashed the app, there's only three possible remaining explanations.
- Maybe this is a new exception that didn't exist before. The solution is to evaluate it using the above guideline.
- Maybe we made a mistake in how we handle this scenario. The solution is to re-evaluate it.
- Maybe our evaluation is correct, and no one along the way has any way of handling this elegantly. The solution here is that there is no better solution available than what you already have.
if (size < 0) {m_size = 0}
exampleisValid
on an object that isn't valid and as a result cause a malfunction of the program. That malfunction could be less obvious but more devastating than an "oops" message to the user and it includes corrupting the user's critical data.Either<Object, Error>
(or whatever the C++ equivalent is) sounds more appropriate.