27

I'm writing an application in C#. I'm facing an InvalidConstraintException, but from that Exception, I seem not to be able to access the Constraint, causing the Exception. That specific question is currently being handled on StackOverflow.

Just out of curiosity, I've written this piece of code (don't laugh):

int a = 2;
MessageBox.Show(((1/(a*a-(a+a))).ToString())); // A simple 1/0 gets blocked 
                                               // by the compiler :-)

This generates a System.DivideByZeroException, but the properties of that Exception don't seem to answer the question "What is being divided by zero?".

Just using "common sense":

  • Action, generating the Exception : The programmer violates a Constraint.
  • Obvious question : Which Constraint?

Or:

  • Action, generating the Exception : The programmer is dividing a value by zero.
  • Obvious question : What value is being divided by zero?

However, those "obvious questions" seem not to be answered by the design of the Exception objects. Does this mean that Exception objects are not based on this question? If not, what's the idea behind Exception design?

Edit: not a duplicate
My question is not a duplicate of this other question: I'm not talking about the message of the Exception, I'm talking about the properties for certain types of Exceptions, which seem not to contain relevant information.

Extra edit: how I would like my source to look like:
(This is an answer on the comment of Thorbjørn Ravn Andersen)

In my original question, I would like my code to look like this:

try
{ 
    dt_MainTable1.Rows[0].Delete();
}
catch (InvalidConstraintException ex)
{
    ex.violated_Constraint. ...  // I would like the exception to give me
                                 // access to the violated constraint.
    ...
}
Dominique
  • 1,846
  • 2
  • 18
  • 28

14 Answers14

44

"What value is being divided by zero?" is irrelevant - in C#, there is no legal integer value which is allowed to be divided by zero, and even if there were such a value, I can hardly imagine how this information would be useful to find the root cause of this error. The error is caused by the specific value of the divisor, not by the value of the dividend.

For finding the cause, I see mainly the following pieces of relevant information here:

  • the kind of arithmetical operation which went wrong - the type DivideByZeroException shows this clearly. In fact, one may interpret this as the exception telling about the value of the divisor.

  • in which line of code did the division by zero occur, and in which context was it called, which is provided by the StackTrace property of the Exception object.

  • the expression which formed the divisor, in case it is not evident from the specific line of code. This is something currently not provided by the C# runtime environment, which could make debugging a little bit easier in certain cases. However, I did not really miss it in the last decade (and I wrote quite some code for numerical calculations).

So for this case, the design of exceptions in C# is ok for most practical cases, it provides almost anything meaningful for propagating the error to the outer layers, but still could be improved.

In the case of your Stack Overflow question, the situation is a little bit different. An InvalidConstraintException could be filled with specific information about the violated constraint using the Message property and the Data property, so the design of the Exception class provides the necessary tools. As you wrote in the SO question, the name of the violated constraint is somewhere in the Message string, which is not ideal to process it programmatically. I did not check it by myself, but I assume the Data property contains no related information, since otherwise you had found it there during your own investigation.

But if code which throws this exception does not make full use of these properties, then you have to blame the design of that code, not the design of the Exception class for it.

Doc Brown
  • 218,378
7

The idea is the framework provides a basic set of common problems as exceptions and you, the developer, provide the details in the exception message.

It is hard to provide useful, context related information in a generic way. So this is left up to the one who throws or rethrows the exception.

Martin Maat
  • 18,652
6

Exceptions allow for more elegance in flow-control around both producing errors and handling those errors:

  • Exceptions are more sophisticated than simple error return codes because they un-wind the function call stack until the exception is either handled within a suitable catch block, or the stack fully unwinds and exits the application without being handled.
  • A function which produces an error does not need to use its return statement nor parameters to pass error statuses back to its callers (A function returning void with no parameters may throw errors).
  • A function which encounters an exception does not necessarily need to be burdened with handling nor recovery from that exception, or even knowing that the exception exists (it can just let the exception "pass through").
  • Error handling and recovery may take place further away from the original source of an error can allow for much cleaner error-handling code.
  • Semantically, exceptions are always seen as errors, so unlike return 'error codes', the intent of code which catching an exception is typically always clear for somebody reading the code.
  • Managed runtime environments and debuggers can observe when an Exception has been thrown
  • A single catch block can handle many different types of errors which may potentially be thrown from many different sources, allowing error-handling and recovery to be treated as a separate concern in a single place.
  • Many languages have features such as finally which can ensure consistent resource clean-up between exceptions and happy-path.
Ben Cottrell
  • 12,133
5

There are many kinds of exceptions, so it also matters how you might use that information in this base cases.

  • Exceptions that you expect to catch and handle should absolutely contain all the relevant details about the source that you don't know yet. For example the WebException provides the response that generated it, if it exists, but not the request, because it is assumed that you should know quite well what the request was if you intend to actually handle the exception. Likewise, IOException doesn't contain the name of the file that you intended to open, because obviously you know it when its one of the arguments!

    Why not store it, just to be sure? Well, as the saying goes, every feature has to be argued for, not against. In this case, it just gives you an information you should already have access to and which you can easily add if you wish to, and that's not enough.

  • The other kind of exceptions are those that you shouldn't catch but prevent, so observing them is an error on your part. However, this also applies to the first category when you forget to catch those exceptions. You cannot handle those exceptions anymore, so either you get to debug the software immediately (in which case the debugger already tells you everything you know), or your only option is to log the exception and hope that someone notices and fixes it.

    It's not the framework's job to add all the details that you might need to an exception when it is thrown, it's your job if you need them, because it's also only you who knows what to include and what not to include. If you store the exception in a serialized form somewhere, what if one of the relevant pieces of information was a password of some user? Yes, it might be useful to the programmer if it was indeed the source of the exception, but the framework shouldn't be the one to decide that. Its only job is to provide you enough details to find out what the error was and where it was raised. The stack trace (and in case of ArgumentException, the parameter name) gives you that and the rest is up to you to collect.

IS4
  • 215
  • 1
  • 7
3

TL;DR: I agree, there could be better information in these exception objects. But be careful how you use that information.

Exceptions have (at least) a two-fold purpose:

  1. To inform the caller that the requested method call failed, so it doesn't uselessly continue and operate on garbage data.

  2. To enacpsulate all information useful for the developer or sysadmin analyzing the cause (typically from reading a log file).

I agree that for the second purpose, including constraint information or the dividend would be helpful in many cases.

But only for the analyzing human! My strong opinion (alas, not shared by everybody) is that a calling method should not try to reason about the cause of failure of some nested sub-call, as communicated by the exception, and decide on different actions based on that distinction.

  • A method should always do its best to fulfill its contract, if possible. So, if it has to throw an exception, it communicates that it wasn't able to do so.
  • Now, the good practice of encapsulation means that the knowledge about how to do one job is inside the method that does the job, and nowhere else, making this one method the expert in doing the job.
  • If some (direct or indirect) caller reasons about failure causes inside some nested call, that's clearly a breach of encapsulation: it assumes that this caller knows enough about the internals of the failed job to decide about actions to be taken.
  • Something like that is only acceptable if the caller knows a different way to do things if the first attempt failed, e.g. if a cache access failed, switching to a direct query of the underlying storage.

So far, I have not seen valid cases where a caller legitimately would inspect an exception object to decide about different actions. Even the distinction between different exception classes in catch statements is a code smell, to me.

3

What matters most is not that you are throwing an Exception.
What matters most is that [you hope that] some code, somewhere, will be able to handle that Exception.

If all you're doing is popping an "error message" out there for a User to read just before their Application disappears in a Puff of Logic, then any old Exception will do. If, however, you think that some other code might be able to do something useful about the Exception (i.e. "handle" it) then your Exception needs to supply sufficient information to allow that code to do so.

Just "trying again" - a valid remedial action in some cases, doesn't need much to work with.
However, If the handlers of your InvalidConstraintException need to know about the Constraint itself in some way, then you need to pass some or all of it along, as a Custom Property on the Exception that you throw.

Of course, this means that you're now exposing part of the inner workings of your code for somebody else [to have] to worry about. That's not always a Good Thing. Allowing other Developers at your own company to have this is fine, but you probably probably wouldn't want it "leaking" out of your domain, to client Applications using your library, say. That's when you would choose to catch the Exception at the Process Boundary and "sanitise" it, making it "safe" for the "Outside World", which might well reduce it to a generic, "Oops! Something went wrong", message-only, Exception again.

Phill W.
  • 13,093
3

The Exception object is the base type and encapsulates the barest minimum that an exception needs (including the information that it is an exception in its class name). As the base class, it does well to be as minimal as possible, to not make the implementation of subclasses more difficult than possible, but as powerful as necessary to get all features out of the exception system that one needs.

In object-oriented programing, you can always subclass this to add arbitrary information. There is no overarching reason why this is not done for any specific instance you are quoting. Or in other words: the programmer of InvalidConstraintException was either lazy, did not think that it would ever be useful, or intended this class to be generic enough that it can be used for any kind of constraint in any context. Most likely though, the information is simply not there. Databases (and that exception does seem to be thrown in the context of a database query) are chronically parsimonious with the information they are giving - often on the database communication layer there is literally only a single integer which encapsulates the problem.

The latter issue is also likely for a division by zero - this problem may occur at a low level, where the relevant source code lines simply are not really availably anymore; representing the actual point where the error occurs in a way that is syntactically similar to the original source may be very complex indeed; and in the worst case impact performance and resource usage even for all non-erroneous mathematical operations. This would be a quite prudent reason to not include more specific details.

AnoE
  • 5,874
  • 1
  • 16
  • 17
1

When I am creating an exception (whether it is simply a message that goes into a standard exception or something that inherits from Exception), I consider the same things: what information will be needed when debugging this exception.

That means that sometimes I include what variables held what value, and sometimes it’s a more generic InvalidOperation exception.

When it’s an exception that will be thrown back up an API call stack, where I won’t be the one writing the code or debugging the issue, I don’t include any details that are going to be irrelevant and useless — such as the name of variables that the programmer will never see.

For your particular example, the message (or the data) might have the denominator or it might not. It would depend upon whether I thought it help me set a break point or recognize where it came from. I would be more likely to include the name of the parameter or variable that held the value zero.

But note that is if my method was throwing the exception instead of it being thrown by the CLR which is what would be happening 9 times out of 10.

As for your dB question, I would include the name of the constraint as part of the message not as data. An exception is not expected to provide details sufficient to implement an alternative method.

jmoreno
  • 11,238
1

The main goal is to avoid cluttering the application with error checking.

For example imagine there's an application which writes 100s of different things to disk. Writing to disk is usually successful, and you may not want to have 100s of places in the application which check for a disk error return code. An "exception" solves that problem: the application writes without checking, and in the unlikely event there's an error then an exception is thrown (and maybe caught in one place).

When you're debugging it's often enough to know the type of exception and the location from which it's thrown (i.e. the call stack) -- for example a bug report or crash dump which says that System.DivideByZeroExcxeption is thrown from ...

MessageBox.Show(((1/(a*a-(a+a))).ToString()));

... is pretty self-explanatory.

If you do need data values also, you might get that from another source -- for example perhaps the application has a configurable log file into which it can log all its input data -- or if you "sanitize" (or error-check) your own input data, then you might add the data value to your own exception message:

if (a == 2) {
    throw new InvalidArgumentException($"Input value {a} is not supported");
}
ChrisW
  • 3,427
1

It can be helpful to remember that an exception is simply exceptional control flow. Its control flow that was so unusual that it wasn't worth handling right then and there. C was full of that sort of thinking:

int err;
err = doSomething();
if (err == 0)
{
    // success - keep going
}
else if (err == 1)
{
    // error: out of memory
}
else if (err == 2)
{
    // error: could not open file
}
etc...

In this case we handle all of the cases. However, this involves explicitly doing this check right at the call site. This can make the code harder to read, and takes a lot of typing.

What we found, over the years, is that often the code executed in response to an error code was a "get things back on track" behavior. It wasn't too concerned with exactly what happened and the best way to fix it, but instead behaved more like a triage approach. You know something went wrong, and you'd like to work around it.

So we attached the concept of handling this kind of control flow onto the call stack. We have the idea of the caller being able to possibly recover from a situation where the callee cannot. And this lead to how we pass exceptions along.

I think the key to your questions is that when an exception is handled, it may be quite far up the call chain from where the error actually occurred. Questions like "which expression divided by zero" don't have much meaning when that expression has long since been removed from the call stack.

So when you throw an exception, you have to decide how much information is needed by the caller to recover. In the case of DivideByZeroException, it was decided that there was no useful information to pass. In other cases, there may be information that the caller has which can be provided to the callee before unwiding.

Choosing how much information to put on the Exception is an art, just as much as selecting any data structure is an art. Too much information and it becomes too difficult to throw the exception -- you have to collect an onerous amount of information before throwing it. Too little, and your caller is left scratching their heads.

And this is why many languages, including C#, have a concept of an "inner" exception. Quite often it makes sense for an intermediate layer to provide context as to when and why an exception occurs, and throw that information up the call stack. But it also may not know everything about the underlying error. So it attaches the underlying error as the "inner exception," and passes up a more convenient exception class with the information you are seeking.

Cort Ammon
  • 11,917
  • 3
  • 26
  • 35
1

Many of the other answers are justifying the approach of an existing exception mechanism. That's fine, but it can also be illuminating to compare exceptions to other approaches and ideas.

In particular, consider the stack trace attached to an exception. We can generate a stack trace at any point in a program, and it's common to think of them as the history of the computation (i.e. "how did the program reach this point?"), but that's not quite right. A stack trace only tells us about calls which have not yet returned, i.e. those waiting for the current step to finish. Hence it can be more illuminating to think of a stack trace as the future of a computation (i.e. "what will resume after this step returns?").

In your example, the stack will look something like the following, where each line is a stack frame ('_' indicates where to 'plug in' the return value of the line above):

0
assertNonZero(_)  // I've made up the name of this step, for illustration
1/_
_.ToString
MessageBox.Show(_)

Notice that the multiplication, addition and subtraction doesn't appear, since they've already finished. The call stack shows us what remains to happen in the future, which is also called a continuation. The above stack is waiting to perform a non-zero check on the divisor, then perform a division with that checked divisor, then perform a string conversion of that result, and finally show a message box with that string.

Throwing an exception will bypass the usual return mechanism (instead, the stack will be popped until we find a matching catch handler). In this example, the DivideByZeroException is thrown by the non-zero check, once it's called with the value 0. Hence one reason the exception doesn't contain the dividend (the number 1 in this case) is that the exception is thrown before we reached the division step (where the 1 occurs).

As an extreme example, let's imagine we alter your example a little:

MessageBox.Show(("The result is: " + (1/(a*a-(a+a))).ToString()));

This involves another piece of data, the string "The result is: ". If we want the DivideByZeroException to contain the dividend of the division step (which would have happened, if the exception were not thrown), do we also want it to contain that string (since we would have prepended it, if the exception were not thrown)? That seems less reasonable, but it highlights the fact they're both just data for steps that the exception prevented from happening.

Note that the stack trace does contain a frame for the division; and the altered version would also contain a frame for the string append. Hence we could argue that including the dividend in a DivideByZeroException is a poor solution, since it's an ad-hoc decision, it requires co-ordination with the exception's consumers (via the public interface of DivideByZeroException), and we must face similar decisions every time we implement a new exception type. A more elegant solution would be to stop excluding arguments from the stack trace: that solves the problem for all exceptions, and gives callers a unified interface to access any data (via the frames of the stack trace).

The reason argument data is excluded from stack frames is probably a mixture of performance and encapsulation, and may vary between languages. Interestingly, if arguments were accessible from stack traces, we could implement resumable exceptions, e.g. our exception handler could replace the 0 in the above stack trace with a different number, and calculate what would have happened in that case.

Resumable exceptions aren't a common language feature; however, they are closely related to coroutines, which are becoming popular. In particular, throw is similar to yield, try/catch are similar to async/await, and all of these can be generalised to shift/reset (the latter implementing "delimited continuations")

Warbo
  • 1,225
  • 8
  • 12
0

I would like to answer from the point of view of:

I'm facing an InvalidConstraintException

What's the logic behind the design of exceptions?

  • And that you are facing a X->Y problem, clearly

The exceptions are low-level entities capable of providing minimal information about the failed execution of a block of code. In order to create a meaningful exception, the code that generate the exception (be it your application, the runtime, or the database library) is only capable of using information in their context.

As for the InvalidConstraintException, specifically caused by violation of a foreign key constraint in a database, you should remember that there are multiple different DBMS-es on the market, and that each has its own communication protocol (the DBC driver).

The SQL DBC driver is not standard, but exposes uniform and standard interfaces. SQL Server speaks a machine language very different from Postgres and Oracle, and so on. And the same applies to error codes.

The database is not required to speak over the wire what is the name of the constraint you violated. Often, it is in the description. That's why ConstraintViolationException doesn't care about a machine-readable form of constraint name or a reference to the constraint object.

What is worse is that, by reading your SO question, it looks like you are violating some code engineering principles.

Normally, when testers/coders encounter a ConstraintViolationException, it is universally assumed that they will see the error message, accept that there is a bug in the software, and take appropriate coding actions to handle it.

You, instead, are trying to bypass the constraint if you aren't happy with the result of your query. This is typical of boss looking at a spreadsheet and asking "I'm not happy with 2+2=4 formula. Type 5 in the cell and go ahead".

Constraints exist for a reason. If you really need to perform a deletion, you should first delete the dependent objects, not drop the constraint to make some super-user happy.

-1

To my humble way of thinking, the term, "exception," has always been "a bit of a stretch," because it really describes two very different cases:

(1) Not anticipated by program logic: "divide by zero," "square root of negative one." Maybe also: "read past end-of-file," "file does not exist."

(2) "You're Dead, Jim ..." (shameless back-reference to Dr. "Bones" McCoy [RIP ...] from the original "Star Trek" series) ... any identifiable situation in which the application might find itself, from which it cannot continue.

The first set of exceptions are the most difficult to deal with since they cannot be classified. Therefore, a very good strategy is to enclose all such areas within a blanket exception-handling block which might at least serve to surround the event within an identifiable exception that will thus serve to locate it. In this way, the programmer-sleuth will at least have a clue where the problem is.

The second group of exceptions – those raised by the application itself – actually represent a very-favorable "alternative path" to the usual if/then/else/while flow of control: they provide a way to "beam me up, Scotty" while still landing on your feet. The logic which has just-now encountered a situation from which it cannot now reasonably continue is able to instantly teleport itself into "a brand-new but still stable state," and to deliver a payload which describes just how it got there.

(And, in most languages, a finally construct provides a way for various pieces of code to react to the presence of the exception as it sails by, without actually intercepting it.)

-1

Okay - another way to think about "the value of 'exceptions'" is to consider what we used to have to do: we could only "return a nonzero return-code."

But this meant that every subroutine-layer that led to our function being called must now recognize that our return-code was nonzero, handle it appropriately, and "formulate an appropriate nonzero return-code for its caller." If any layer "got it wrong," we now had "a bug" that was extremely hard to find. Furthermore, we had no clean mechanism to communicate details about what went wrong.

The "exception" mechanism evolved into a generalized solution which could handle not only "division by zero" but also anything else. It did so by cleanly separating the logic which identified an "exceptional situation" from the logic that would handle it. And, by "simply bypassing" the call/return logic that had led up to the point where the exception was identified, it rendered all those potential problem-points entirely moot. When an exception is "thrown," control transfers immediately and directly to the chosen "catcher," and the "thrower" also has a clear opportunity to attach a message to the baseball.

The already-existing "class hierarchy" in our programming languages also played very-nicely here: Exceptions are Classes. Therefore, they have Subclasses, and any Exception that we throw figures somewhere into that class hierarchy, allowing our "catchers" to be as specific or as general as we like.