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.DivideByZeroExcxeption, 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.
    ...
}
17
  • 3
    Does this answer your question? Why do many exception messages not contain useful details?
    – Kevin
    Sep 20, 2021 at 19:43
  • 3
    Is this more practical or philosophical? It seems as if you've gone from "I can't read this particular constraint exception" to "is there a trick to reading exceptions which everyone knows except me?" Sep 21, 2021 at 16:36
  • Is your question about division by zero or about exceptions in general? You might want to clarify the question with respect to that.
    – usr
    Sep 21, 2021 at 17:52
  • 6
    This question seems to be asking "why did the C# BCL designers implement InvalidConstraintException the way they did", and the only people who can answer that conclusively is them. As they're unlikely to answer here, this almost certainly means that any answer posted here will not be by said people, which implies said answer can only be speculation and/or guesswork i.e. opinion, which means this question is off-topic and should be closed.
    – Ian Kemp
    Sep 22, 2021 at 14:27
  • 1
    @IanKemp: My question is indeed "Why has this been done like this?", which automatically might lead to "If you do that yourself, you might consider taking this into account", which makes it less off-topic than you might think at first.
    – Dominique
    Sep 23, 2021 at 6:08

14 Answers 14

4

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.

1
  • 1
    "I agree that for the second purpose, including constraint information or the dividend would be helpful in many cases." - constraint information: yes. Dividend: definitely not.
    – Doc Brown
    Sep 20, 2021 at 18:15
43

"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.

19
  • 15
    Agree, knowing that it is a 1 which gets divided by 0 is hardly useful. The interesting part is figuring why the divisor is 0, since this (presumably) is the unexpected part.
    – JacquesB
    Sep 20, 2021 at 17:12
  • 7
    "in which line of code did the division by zero occur, which can be evaluated from the StackTrace property of the Exception object." - code often has multiple division operations in the same line, so more specific information than just line number is often useful. For a simple example, 2/(1/a+1/b) computes a harmonic mean of two numbers. Each of the 3 divisions could be a division by 0. You could split the 3 divisions across 3 lines, but that'd be a lot of line bloat for such a simple operation. Sep 21, 2021 at 0:58
  • 3
    @user2357112supportsMonica: ok, there may be such cases, but if that happens to occur "often" is quite debatable, looks more like an edge case to me. And just because this edge case could require more information than provided in a standard C# exception, I would not draw the conclusion the Exception class has a design flaw, what this question was about.
    – Doc Brown
    Sep 21, 2021 at 3:07
  • 19
    I have to charm in on the "more information" thing. In the Java world, Kotlin has added the name of the variable that is null to NullPointerExceptions. Has been very well received and a couple years later, the major JVM implementations have managed to follow. Code like a.b().c(d(e), f) is very common and just NPE is not very helpful. NPE: d(e) is null is so much better. And i feel the same re. this exception. Sure, its mostly fine, but it can be improved.
    – marstato
    Sep 21, 2021 at 8:04
  • 2
    @dan04: Indeed so. Still, interesting that modern systems don't report error locations as precisely as systems could 35 years ago. Actually, the way "line numbers" are handled in debug information in most systems limits the effectiveness of partial builds, by making it necessary to rebuild portions of the code where the only thing that has changed is the number of preceding source lines. If line numbers could be reported relative to the start of a named function, and the current source code for a function (with such adjustment) would generate identical first-pass output except for...
    – supercat
    Sep 21, 2021 at 19:55
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.

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.
4

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.

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.

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.

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.

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");
}
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.

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")

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.)

7
  • The term makes sense in its origin context of design-by-contract: It is an exception to the rule that input X produces output Y. Sep 21, 2021 at 3:22
  • 5
    "Not anticipated by program logic" doesn't really make sense. The chief reason for exceptions is that you can catch them higher up in the call stack, but that requires anticipating them. If you don't anticipate handling the problem, you might as well exit() the program. That's also the same course of action for the second case.
    – MSalters
    Sep 21, 2021 at 10:04
  • @MSalters: If one is trying to e.g. deserialize an object in response to a "File Open..." dialog, it's not possible to anticipate all of the things that might go wrong, but which should be handled by presenting an "Unable to open that file" dialog but continuing program execution. Unfortunately, I don't know of any language or framework that does a good job distinguishing that type of exception from those which should terminate the application.
    – supercat
    Sep 21, 2021 at 14:51
  • I'm not a big fan of the wording in this answer, but @MikeRobinson does make a good point. An FileIoException is very different from a StackOverflowException. In the former, you may be doing network file access and expect that occasionally, the access may through an exception. You put the work in a try block and, if the exception is caught deal with it (perhaps by retrying). If you get a StackOverflowException (or an out of memory exception or a few others), well, then "you're dead Jim" - there's nothing you can do. If you catch one blindly and try to process it, then you're screwed
    – Flydog57
    Sep 21, 2021 at 17:26
  • @Flydog57: There are many reasons deserialization may fail even if all of the bytes of a file can be read. If the object that is being built during deserialization is corrupted as a result of invalid data, but the partially-constructed object ends up being jettisoned without being used, the fact that the object was corrupted shouldn't matter. As for StackOverflowException, I think programming languages really fall short in failing to allow recursive parsing functions a means of bowing out gracefully before blowing the stack, without having to guess what recursion depth would be safe.
    – supercat
    Sep 21, 2021 at 20:00
-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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.