Beautiful errors
The following code in a recent PR caused it to be rejected, can you figure out why?
The error clearly states that what the error is, but it fails to provide crucial details. Namely, which files have been corrupted. If I’m seeing an error like this in my logs, I need to be able to figure out what happened, and not hope & guess.
I’m picking up on this particular change because I found myself tallying in my head the number of comments I make on PRs from our team, and quite a large portion of that involve these kind of changes. What I’m looking for with error handling is not just to do it properly and handle all edge cases, but to also properly report it so the person who will end up reading this error message (very likely years from now) will have a clue about what they are supposed to do now.
Sometimes we go to great lengths to ensure that this is the case. We have an entirely different server mode dedicated to handling catastrophic errors, so when you try to get into RavenDB, you’ll get a meaningful error page that will at least try to give you an idea about how to fix this issue, for example. The sad part is that it is very easy to have a single error sour up a really good experience, because it doesn’t provide you with the right information to fix it.
We spend a lot of time just crafting errors properly. They go to the log, they are sometimes blocking the UI (if the server cannot start), we have dedicated alert system that handles error and alert distribution across the cluster so an admin can get into any node and see all the stuff that they need to know about across the entire cluster.
Comments
It would be nice if .NET itself took this approach. I'm looking at you,
InvalidCastException
...Alex! Yeah! Or how much better would it be, if
NullReferenceException
would tell you the file, line and/or variable name ... or anything at all!Well, I guess there are practical considerations.
With casting, the important pieces of runtime information are the 'to' and 'from' types, which should certainly be available. Perhaps it was deemed too costly in the general case to format this information into an exception?
The null reference case is a bit different since the only runtime piece of information is 'something was null', which it's telling you. The file and line should not be needed because you can determine those from the call stack, unless it was thrown from a compiled expression tree (I see this a lot with AutoMapper). As for which variable, that should be available at compile time but would probably need to be formatted into the message at runtime because the exception message is probably localised. Performance/complexity concerns, maybe?
Given a call stack the source of a null reference exception quickly unless the statement involved contains a lot of dereferences, which in the early days of .NET you wouldn't usually have unless you were tolerating very long and complex one-liners. But when C# 3.0 added object initialiser lists it also encouraged bundling a lot of lookups into one statement, which makes the humble NullReferenceException a time-sink.
Exceptions are way better than error codes, but indeed they could be more useful. It would have saved me weeks if a dictionary with a primitive key would include the missing key in a KeyNotFoundException. Runtime cost is not so much an issue; an exception is expensive anyway. If it saves a minute in troubleshooting, it's definitely worth it.
Exceptions are incredibly expensive, even the setup necessary at the level of not having to handle them. So I agree with @kpvleeuwen, if you are going to throw, at least give as much information as possible.
Definitely agree. Code that is about to throw an exception should take as long as it likes and format a really helpful message. Exceptions should not be used in place of API. They should only be used in scenarios where something is terribly wrong.
@Alex As far as I can tell from this CoreCLR issue, casts that use the
castclass
IL instruction always reported the types. And casts that use theunbox.any
instruction report them on .Net Core 1.0 (any maybe also on some future version of .Net Framework).Are there any remaining cases where the types are not reported on .Net Core? If so, consider creating an issue about it.
@Fabian
NullReferenceException
is not that special, it will tell you file and line numbers if debugging information (i.e. a PDB file) is available, just like any other exception. And VS 2017 will even tell you which variable wasnull
in Debug mode.KeyNotFoundException : )
@Svick: it's really nice that CoreCLR includes this information! I'm looking forward to being able to use that.
Unfortunately most of my development is still done on the full-fat .NET Framework (sometimes even as far back as .NET 2.0) for various legacy reasons :P
@Federico,
exceptions aren't that expensive. See this article. Yes, they are about 15x slower than return codes, but still talking nanoseconds....
I tend to make the messages explain in detail what went wrong with what. File and line info is added when needed, mainly when debugging. In production environments I normally create the message, log it using callstack info (configurable) and throw the exception with the constructed message. That way the user might see a proper explanation of the error, and the developer can do some log-diving to get the callstack.
Beautiful errors are a great start, but I would love to see languages provide better ways to provde context.
Better error messages help add context (What file was being processed? What line in the file produced the error?). But, errors can only know so much about the context they are run in. (and that's not a bad thing!)
A stack trace is another form of context. But, often, they lacks critical details needed to get a better context. (Why was this file being processed? Was it pulled in by another file, for example?)
But, imagine a mythical language construct
context
.If an exception is thrown in the
// Do file processing here
block, any context found while unwinding the stack would be added to the exception.Effectively,
context
in this use would expand to a form somewhat like this:The error message would then read of the form:
Now, regardless of the exception that is thrown, you'll know this exception happened during the processing of a certain file. This is information a stack trace would not have given you - you would have only seen the line number. While this is something you may have been able to add to the original exception message, it lifts the burden from the thrower of the exception to provide it. (Plus, what if, say, it had been a NullReferenceException? Now you know processing a certain file caused that.)
@Robert Isn't proper logging a better solution for that? Some logging frameworks even allow you to use nested contexts. That does not modify the exception itself, but examining the log should give you all the information you need, certainly more than what you would get from the context you propose.
Robert, There is also a non trivial performance cost here, since you'll need to prepare the error in all cases, which does an allocation in the happy path. It is also asking the user to be consistent about error handling, which is something that we know users are bad at.
Including extra runtime information in an exception likely carries a performance cost even if the exception is never thrown: additional code is required to copy that data into the exception, and that additional code takes up space in CPU caches.
Although, I suppose you could do something like the x64 exception implementation(?) where a separate lookup table is consulted after the exception is triggered in order to locate the appropriate information on the stack/heap, thus allowing the extra diagnostic code to live elsewhere and stay out of the CPU cache on the happy path.
@Charles: According to the article you linked to exceptions are not 15x slower; they're around 20000 - 100000 times slower. And sure, if you then only throw once every 2700 calls, then you're only paying a small fraction of that cost; which in one benchmark was 15x. But that's not because exceptions aren't that expensive, it's because they're used very sparingly in that benchmark.
Note further that the cost of exceptions is situational; it's not just the stack trace; there are lots of factors that can have a dramatic effect; so I wouldn't expect too much precision in that estimate.
Regarding the
NullReferenceException
points, your own code should never be able to hit these because you've got guard clauses in the way that throw ArgumentNullExceptions. Your IDE / fxcop / R# / static analysis tool of choice should find and prevent these errors for you.FXCop rule
@Charles, @Eamon. The 15x figure in that article is quite nonsensical. The return-code example writes two values (perhaps only to registers, I don't know the x64 calling convention), and the exception-throwing example does a non-inlined call, a memory read, a memory write, a modulo, a comparison, and a 1 in 2700 exception throw. The modulo alone can probably explain much of the difference
Comment preview