> I have written plenty of C# code that deals with exceptions, and it is a pain to do it correctly. Apart from null, it might very well be the biggest design mistake of the language.
In user code (as opposed to framework code), Typed Exceptions are overrated.
Let's take these two cases:
1. End-User: Doesn't care if the Exception is a ParseException or an InvalidVersionException. Exception("Invalid number") and Exception("Version should be higher") is just as good enough for a message. Basically, for the end-user, the app crashed and that's it.
2. Programmer: Will have to open the debugger or check the code anyway.
I get it that it's useful in some cases, when you wanna send back some structured data to the caller. But in the vast majority of cases, the plain Exception class and Stack Traces suffice.
Agreed. I rarely need a typed exception. The purpose of the exception is to say "Stop doing what you were doing, and head up the stack until you find a point where the work can recover from this and carry on."
Generally speaking, that point doesn't care why the bunch of stuff five levels down went wrong. It just wants to report the error, and then either carry on with something, or let the user know what the problem was and decide if they want it to carry on.
Very occasionally, the actual type of the exception is useful. But mostly I just don't care about typed Exceptions.
So this is more akin to panics, where you don't really care about handling the error but rather want to stop dead in the tracks. But take something like .NET's SocketException, which contains a SocketErrorCode property; you may very well want to take different action depending on the type of error this is. This may be as simple as changing the text of the error shown to user (with suggestions on how to fix), but could also be some code based recovery that takes different action depending on the type.
Typed exceptions are not only idiomatic but deeply ingrained into the language core. Even basic primitives like loops use exceptions to control flow (StopIteration).
What if you want to handle some exceptions in code? After all, this is why we throw exceptions rather than just call exit() every time something goes wrong. Eg. if this service call time out, we retry a few times. Without typed exceptions we have to perform some kind of string matching on the exception message.
exit() is worst: in winForm, it's generally better to catch the exception on top level and show a MsgBox to the user without crashing. The user understand that his current action has failed, but he do not lost its input.
The author is painting a pretty bad picure of C# error handling that isn't close to reality.
In reality, you would never call uint.Parse and let the exception bubble up, you'd call uint.TryParse and handle the logic for failure in the method. There is almost always a way to write your code so it doesn't throw exceptions. I'm working on a pretty large C# codebase for a SaaS website and by doing "find in all files" the keyword "catch" I came up with 174 references. For hundreds of thousands of lines of code, we have had to manually catch and handle exceptions only 174 times.
Standard example for exceptions you have to handle: IO. You can't work around it. Want to open a file? That might throw (file doesn't exist/is inaccessible). Want to write? That might throw.
Now, we can argue if you can ignore that, in a controlled environment, and get away with it most of the time.. But that's not what the article is about. He compares exhaustive error handling between the two languages.
imho the author paints a very accurate picture of c#. exception handling remains a mystery to many programmers and is a constant source of errors.
error handling in general is hard to do; to have good, what i would call, "error partitioning"- catching errors/exceptions where they can be fixed is difficult- regardless of exceptions.
perhaps you got your error handling right and don't need more catch clauses, but if I was you I would investigate :)
public static UInt32 CheckNextVersion([...])
{
Contract.Requires(previousVersions != null);
Contract.Requires(versionString != null);
UInt32 version;
if (!UInt32.TryParse(versionString, out version)) throw new ParseException();
var min = previousVersions.Cast<UInt32?>().Min();
if (min == 0) throw new NewReleaseImpossibleException();
if (version >= min) throw new InvalidVersionException();
return version;
}
I still wouldn't use this code in production but all the exception handling in the article is completely unnecessary and even throwing those exceptions in the error cases is a design decision. You could as well create your own Result<T> type either containing a value of type T or an error message and return that. Obviously the more support by the language and the compiler you get the better, but you can still do it even without.
public static Result<UInt32> CheckNextVersion([...])
{
Contract.Requires(previousVersions != null);
Contract.Requires(versionString != null);
UInt32 version;
if (!UInt32.TryParse(versionString, out version))
{
return Result.Error<UInt32>("Bad version string.");
}
else
{
var minimumPreviousVersion = previousVersions.Minimum();
if (minimumPreviousVersion == 0)
{
return Result.Error<UInt32>("New release impossible.");
}
else if (version >= minimumPreviousVersion)
{
return Result.Error<UInt32>("Invalid version.");
}
else
{
return Result.Success(version);
}
}
Bad programmers write bad code in general, so basing your opinion of C#'s error handling system on their use of it seems unwarranted. Exception handling does not remain a mystery to many programmers, just many bad programmers. Aside from that, the author is a contributor to Rust so I suspect there could be some small amount of bias here.
> ...catching errors/exceptions where they can be fixed is difficult...
What's difficult? When your program is about to do something like say open a file - if you don't want an exception to be raised from that code, you wrap that code in a try...catch. In the catch block, you take corrective action.
The very simple bottom line with error handling in .NET apps is that you handle/log/notify the user at process and thread entry-points like main and AppDomain.UnhandledException. Other than that, you use a try...catch block.
It's a bit of a reach to sling accusations of bias. The Rust project makes it trivial for people to contribute, so anyone with even a passing interest in the language can become a "contributor to Rust" in seconds. If you try and disqualify all of the 1,000+ Rust contributors from writing blog posts about the language, then you most likely also disqualify everyone qualified to write about the language from writing about the language. :P
I didn't sling an accusation, I said "I suspect there could be some small amount of bias". If I were accusing, I would have said "this person is biased.". Despite what you say, I still think the suspicion is valid.
The irony here is that people on HN do judge you if you live mostly in the C#/.NET world, but you're arguing against my suspicion of people who contribute to Mozilla.
Do you think it's a far stretch to say that people who actively use open source languages and libraries and contribute to Mozilla projects are much more likely to be biased against anything that Microsoft does?
You're still leaping to conclusions, you're just trying to justify it behind a veneer of persecution. I'm a Rust contributor and I think C# is a fantastic language; it's my dream to tempt Anders Hejlsberg away to Rust. :P I'm also typing this from a Windows 7 netbook, my sole computer, though I'm looking to upgrade to a Surface once Windows 10 is here. We have several former Microsoft employees in our ranks and one of our core contributors is currently interning at Microsoft. It is possible to be critical without there being a hidden agenda!
uint.Parse vs TryParse misses the point. It's very true that doing proper error handling in C# requires more diligence and attention. I know people like to harp on checked exceptions in java, but if used properly, they at least help with the discoverability aspect.
The fact that you have 174 catch blocks in a very large codebase only reinforces the blog's point, which is you may be missing error handling blocks or you just have very general handlers.
We don't really do catch-all blocks and our app is monitored on New Relic where we almost always have a 0% error rate (rarely we get <1%, most often related to a new commit that we can fix right away)
The only places where we need good exception handling is when doing I/O (network and filesystem). If you have proper layers to handle those things (with queuing/retrying/logging) you end up with a large codebase with very little places where you have to handle exceptions. Anything that is user-input we use TryParse and we do proper validation on the data before doing anything with it. This method is makes it easier to read because you know what the intent is better than doing parse/catch exception in my opinion.
Oh, I agree that TryParse is much better than Parse for cases where parsing is expected to fail. But that doesn't detract from having better error handling capabilities in places where it is needed (e.g. i/o). A lot of the .NET i/o APIs will throw 4-5 different types of exceptions, and as the article states, you need to cross reference docs to see which exceptions are possible on certain calls. It's easy to forget or miss because compiler will not care.
You don't need to know the types of exceptions that are possible on certain calls. All you need to know is what exceptions you can handle. They are not strongly related.
The ones you can handle is a subset of possibly thrown ones, which means you need to know the superset before deciding which ones to handle. You're just describing the handling strategy: pass on it or handle it. It doesn't obviate the need to know what's possible there.
As an example, imagine you're building an application that you know uses sockets. And it's in the requirements that your application must be able recover and re-attempt set of operations. So you look into what kinds of network and socket exceptions exist in your platform, framework, and libraries. From there you decide which ones you should trap and attempt to retry.
At no point do you need to know exactly what methods trigger what exceptions. Yes, you do need to know the superset of all exceptions. But that's no different than knowing the superset of all methods or all classes. My point is, a mapping of methods to exceptions is unnecessary.
Interestingly enough, you might end up handling exceptions which can never be triggered by your application. But this isn't a bad thing, as this can make your application more robust in the face of change.
>As an example, imagine you're building an application that you know uses sockets. And it's in the requirements that your application must be able recover and re-attempt set of operations. So you look into what kinds of network and socket exceptions exist in your platform, framework, and libraries. From there you decide which ones you should trap and attempt to retry.
You need to decide where you're going to handle them, and this depends on how much context is required to handle them appropriately. Sometimes this may require doing it right at the point where the method is invoked that triggers the exception. Even if you decide to not handle the exception, you may still want to know the exceptions so you can rollback/adjust state/invariants before letting the exception continue unwinding.
But where you decide to handle an exception is independent of what methods trigger what exceptions. The decision as to where to put error handling code is based on the program structure and requirements. If my requirement is to rollback the current operation and retry, the error handling goes where that operation starts.
I consider adjusting state based on the exception type poor design. That's very tight coupling. But even so, where you put that code is still independent of where exceptions are thrown. If your data state adjustment requires an exception type, check for that where that state change is appropriate. But what methods trigger what exceptions is still irrelevant.
Huh, author even uses exceptions as a part of normal workflow, instead of checking if "previousVersions" list is empty before calling Min on it he instead catches InvalidOperationException. I don't want it to look like an ad hominem argument, but...
Anyway, C# authors had Java in sight when designing the language and after some contemplation they didn't include checked exceptions in the language. And their main argument is that in the vast majority of cases ignoring the exception and allowing it to propagate further up the stack is exactly what you want to do, simply because there is nothing you can do about it but to log it and tear down the entire object graph or write an error message to the user.
> propagate further up the stack is exactly what you want to do
This is an important point imo. It is actually one of the ways you can describe to a non-programmer why it can be so hard to write typcial desktop software which never ever crashes (as in 'unhandled exception' dialog): huge amounts of time would be wasted to figure out for each and every single statement what can go wrong and how to handle it. Seriously, I've been at meetings where more time was spent on discussion on how to deal with/present to the user some obscure once-in-a-lifetime error than on actually fullfilling requirements.
I have a desktop app that has a single exception handler at the event loop. It's amazingly robust. That file you're trying to save is locked? Network location went away? Doesn't matter. It displays the error in a dialog box (so you know what went wrong) and returns the user to the app. Whatever operation they were doing, they can just try again. Any cleanup is performed automatically and the application, without really any effort, is always in a stable state.
That's how exceptions are supposed to work. The wack-a-mole attempt at ensuring you "handle" every case is wrong.
The problem, in my opinion, is that most programmers have been so brain damaged by Java that anyone exposed to that way of programming can't see how exceptions are supposed to work. Java not only makes you try/catch/declare everything but also makes you use exceptions for non-exceptional operations. A perfect storm.
Good post. Not capturing a stack trace is a Rust specific thing -- this is easy to do in Scala, for example, and still maintain the benefits of monads + algebraic data types. I'm sure Rust will get similar utilities with time.
The main issue I have with this technique is the higher mental overhead when getting started. That goes away with a bit of practice.
Scala gets stack traces automatically from the JVM though, along with much of its debugging support. The Rust team will have to do this all by themselves, being a natively compiled language and all.
You can get stack traces on panics by running with RUST_BACKTRACE=1.
I don't know that it makes much sense on Error objects though, there's no requirement that they're even able to store backtraces, and collecting backtraces any time an error object is created would be really expensive and would make the runtime a dependency of errors.
Most VMs put most of the overhead of stack creation at where the error is created, so it usually works out fine (throw is expensive, everything else is cheap). Stack traces are usually dependent on whatever debug symbols are loaded, so in the CLR you'll get huge blank spaces if an exception propagates through some dynamically generated or even native code.
Just printing stack traces is a distraction though. The only thing that is required that on an exception, the entire stack above the the exception site can be inspected in the debugger.
And how do you tell where an error originated without a panic being called? Is the programmer expected to just instrument the their code to put the panic in as soon as possible? Anyways, this is a well-known problem with monadic error handling, and one of the main reasons it doesn't get more widespread adoption.
> I'm sure Rust will get similar utilities with time.
Unlikely. Scala gets tracebacks for Try because the left arm stores a JVM Throwable, and throwables reify a stacktrace during initialisation.
For Rust, this would mean:
* Result's Err mandates an Error (currently has no trait bound at all)
* Errors must be able to store a traceback
* Error/Result requires rt in order to collect a stacktrace
* Rather than stack-allocating a struct or two, Err results now require significant heap allocation and data collection completely breaking existing usage patterns
I'm not suggesting `Err` stores a backtrace. I am suggesting that the programmer could choose to store a backtrace in the error objects they choose to store in an `Err`.
I don't do this in my Scala code because I rarely find I need a backtrace.
This will collect a stacktrace in the provided buffer, the result can then be stored in whatever object the developer wants (likely after decoding it to a String): http://is.gd/tOD8Ho
Note that you really don't want to be relying on this unstable feature, it's quite a hack at the moment and will very likely be moved out into an external crate.
As long as Err results are only created under exceptional circumstances, there should be no performance hit right? This is one significant reason why errors should get a path separate from normal return values, but that isn't the case in Rust (or monadic error handling in general, which doesn't really consider debugging).
I love monadic error handling too, but not for the reason that the author cited.
The fact that you know exactly what errors a function can throw is not an
unique feature of monadic errors: in Java, you are required to explicitly list
exceptions that a function can throw (checked exceptions), and you must handle
every exception when calling the function.
The real feature of monadic error handling is that error handling become
composable.
Imagine that you want to write a function that accepts three character-encoded
integers and that you want this function to return the first integer it
successfully parsed.
With exceptions, you will probably use a parseInt function with a type similar
to:
fun parseInt(String) -> Int throws ParseException
And you will write the function like this:
fun getFirstInt(String a, String b, String c) -> Int throws ParseException:
try:
return parseInt(a)
catch e1:
try:
return parseInt(b)
catch e2:
return parseInt(c)
Now take a language supports algebric data types (like Haskell, Rust, Scala or
OCaml). The parseInt function will probably have a type similar to:
fun parseInt(String) -> Result<Int, ParseException>
You will probably define a simple operator to manage the pattern if it fails,
try that instead before implementing the getFirstInt function. Lets call this
operator <|> (it's its name in Haskell):
fun operator <|>(Result<r, e> left, Result<r, e> right) -> Result<r, e>:
match left:
Ok(r) -> return Ok(r)
Error(e) -> return right
With this operator, getFirstInt could be implemented as simply as
fun getFirstInt(String a, String b, String c) -> Result<Int, ParseException>:
return parseInt(a) <|> parseInt(b) <|> parseInt(c)
I think that the fact that you can write you own error handling operator is the
killer feature of monadic error handling (and monadic programming in general).
It makes the enforced handling of errors really painless (while it's like hell
in Java where every exception must be handled).
It's almost impossible to create this kind of operators with exceptions because
they are not a return value, and thus can't be handled in expression. With
monadic error, handling operators are just plain old functions.
Just a note, Rust's error handling is not monadic because Rust doesn't have monads because it doesn't have HKTs, I guess we could call it "reified" though.
Actually it is. Rust Option and Result types implement .and_then(), which is equivalent to bind. They are also equipped with many of the other things you'd expect of a monadic types.
The thing is, Rust's type system is currently incapable of expressing an GENERAL monadic trait/interface, which it would be, if it had HKT. (That would allow implementing all the additional monadic util methods as trait default implementations, and having generic functionality applying to any monad.) Hopefully we get there someday.
Both exceptions and results are important for software architecture. The lack of the former leads to an oversimplification of interfaces in languages with only the latter. The author hit upon an example that is probably better written using results. But guess what? A C# programmer would write it that way too. See the TryParse methods found all over the C# standard library. On the other hand, what would a Rust programmer do to handle an out of memory situation? That would be too pervasive with the result approach so it has been culled from all interfaces. Also, nullable reference types are a separate issue.
The point that you're glossing over it that Rust forces you to acknowledge the existence of errors, either by working with Err types or by using .unwrap() to terminate the thread in the event of an error. The existence of both Parse and TryParse in C# indicates a trend of allowing to errors pass silently by default, which ultimately results in surprise runtime failures.
> On the other hand, what would a Rust
> programmer do to handle an out of memory
> situation?
Let's ask MSDN what a C# programmer would do:
"This type of OutOfMemoryException exception represents a catastrophic failure. If you choose to handle the exception, you should include a catch block that calls the Environment.FailFast method to terminate your app and add an entry to the system event log"
On modern systems it's not possible for an application to know when memory has been exhausted, because of virtual memory and overcommit (http://www.win.tue.nl/~aeb/linux/lk/lk-9.html#ss9.6). The system will happily tell your process that memory is available even when it's not, and relies on the OOM killer to free memory by force when necessary. The only context in which an application can reliably detect OOM is when that application is the kernel itself.
Fortunately, Rust has a stripped-down version of the standard library, called libcore, that exposes features which don't require allocation and thus don't have to worry about OOM. This profile is perfectly usable for writing kernels, and could also be used to write an alternate kernel-specific stdlib which make it possible to handle OOM.
> Complaining about the existence of Parse is like
> complaining about the existence of unwrap in Rust.
This is mistaken. Complaining about Parse would only be like complaining about unwrap if unwrap were the implicit and default behavior. Parse throws implicitly, but unwrap doesn't get called implicitly. The fact that the possibility is impossible to accidentally overlook is an enormous and crucial difference.
Parse is less explicit than unwrap because the following C# code is legal:
uint foo = uint.Parse("3");
But the following Rust code is not legal:
let foo: u32 = "3".parse(); // error: mismatched types
It's impossible to use the return value of parse as though it's a u32, because it's not a u32. You can't accidentally add it to some other number or use it to index an array, because it's the wrong type entirely. You invariably must acknowledge the possibility of failure, and if you're okay with terminating your thread in the event of an error you can do this via an unwrap:
let foo: u32 = "3".parse().unwrap(); // just fine
Rust's parse is even more explicit than C#'s TryParse, because even with TryParse it's possible to forget to check the return value:
uint foo;
uint.TryParse("Hello", out foo); // this compiles and runs!
And it's not just about string parsing. The overarching point here is that not only does the compiler protect you from errors that you know about, it also brings your attention to errors that you didn't know about. The OP has a great example in that it's very easy to forget that the `min` method on vectors can fail if that vector is empty.
Finally, this assurance extends in the other direction as well: knowing that libraries are architected to make errors explicit via return types means that if I don't get yelled at for not handling a return type, then I have peace of mind knowing I'm not overlooking anything. This is somewhat analogous to the considerable assurance given in knowing that types aren't implicitly nullable.
Now, if you're looking for something that can still go wrong, there's a better example then OOM: dividing by zero in Rust will implicitly panic. So while it's unlikely that a library author will use unwrap in their code (there's a strong culture against Rust libraries causing panics), it's possible that a library author could accidentally cause a panic by dividing by zero.
Both Rust and C# provide throwing/failing interfaces for parsing, it's just that Rust's is written:
let foo: u32 = "3".parse().unwrap();
and C#'s is written:
var foo = uint.Parse("3");
Both require familiarity with the stdlib (Parse throws, TryParse doesn't, unwrap fails).
I agree that TryParse is worse than Rust's ADT approach, but that's a different issue, much like nullable references. The author is asserting that results are superior to exceptions and I am asserting that no, that is just a bad application of exceptions. Additionally, I am asserting that without exceptions, you limit what programmers are willing to propagate through their interfaces because errors become viral.
"Can this method throw? Should I catch a PathTooLongException here? And while the .NET framework is documented to remarkable detail, this is rarely the case for third-party libraries or in-house code. At the bottom of the call stack, you just have to assume everything may throw. "
Isn't this true for all languages in one form or another? Go and Rust have "panics" Any code might fail exceptionally, stack overflows and out of memory conditions come to mind as relatively common ones.
The difference here is that C# treats more kinds of things exceptionally than Rust or Go. This seems to have more to do with Rust and Go ability to more easily return multiple types from a method/function and a philosophy of opt-in vs opt-out on handling an error.
I am used to C#/javascript so exceptions with try catch are very natural to me. Writing robust programs always involves handling everything one way or another. Even out of memory conditions can be handled and recovered if your always thinking that everything can throw.
The null issue is interesting, I like null, I like undefined with null even better in javascript. I always seem to need to encode something has no value or something was never even set vs picking a valid value as a special "no value" i.e. int.MaxValue or whatever. The only thing I would change in C# and javascript is null lifting object.property should not throw if object is null same with foreach over null object. They are adding the ?. for that but you need to explicitly do it, NullReferenceException is far to common in C# from those two situations explicitly. I belive Objective-C did some kind of null lifting where sending messages to null objects don't throw.
> Isn't this true for all languages in one form or another? Go and Rust have "panics" Any code might fail exceptionally, stack overflows and out of memory conditions come to mind as relatively common ones.
The concern does not make sense in Rust, Rust's panics can not be caught and will kill the current thread.
> The difference here is that C# treats more kinds of things exceptionally than Rust or Go.
Or it treats exceptions much less exceptionally than they do (Python being even further along on that axis), a C# exception is at best "uncommon".
> This seems to have more to do with Rust and Go ability to more easily return multiple types from a method/function
Rust doesn't use MRV for results, Result is a single struct (technically an enumerated type which can contain either a "valid result" or an "error result", not both).
> I belive Objective-C did some kind of null lifting where sending messages to null objects don't throw.
Not in the sense you think, Objective-C's nil is simply a black-hole, messages send to it don't do anything and return nil. It's possible (though not probably not recommended) to opt Smalltalk or Ruby[0] into this behaviour, because nil is a standard object not a magical special value (and both languages have open classes).
Things will go wrong. Some subset of things that can go wrong are things you want to do something about (e.g. file doesn't exist -- maybe prompt the user for a different file). The remainder are things you don't want to deal with (e.g. out of memory -- in most cases this is very rare so you're ok to just crash).
You want to arrange your program in such a way that you must deal with the errors you want to deal with. Your code won't compile if you don't. That is what this technique brings.
If you don't want to deal with an error then you unwrap it, which will terminate your program just as well as an unhandled exception would. The salient difference is that you can't accidentally unwrap something, whereas you can accidentally forget to handle an exception.
The set of exceptions that can be handled is vanishingly small. With exceptions you should be catching exceptions very infrequently and in key places. Talking about accidentally forgetting to handle an exception at a method-call site completely missed the point. That's not how one should code or think about coding when using exceptions.
This is false. I use the pattern in the linked blog post everyday so that my code must deal with the errors I want to deal with and can ignore the rest.
Code littered with try! macros does the same thing as propagating exceptions but you have to be explicit at every call. You have put error handling code literally everywhere and then just achieve the same result that exceptions do automatically.
I understand Rust's motivation for handling errors this way and it makes sense for the type of language it is. But in terms of computing science, it's a step back into the past. Just like manually managing memory.
I find the opinions presented in this article are far too common these days in completely misunderstanding exceptions.
"The consequence, when you want to write robust software in C#, is that you have to keep an MSDN tab open at all times. Can this method throw?"
First of all, assume every method can throw. Just do it. It's either true or could be true in the future when someone changes that method. Maybe it doesn't use the network today but tomorrow it calls out to a webservice. So all methods throw. And forget knowing what exceptions they might throw. That's an implementation detail and we want to hide implementation details.
"Should I catch a PathTooLongException here?"
No, you shouldn't. What are you going to do any differently with that than you would FileIsLockedException or NetworkIsDownException or HarddriveIsCorruptException? You wouldn't do anything differently. Show the user the ErrorMessage in the dialog and allow them to restart whatever operation was in affect. Let them choose a shorter path, close Excel, plug their Ethernet cable back in, or whatever. Trying to play wack-a-mole is a fools errand. Put MSDN away, you don't need it.
> No, you shouldn't. What are you going to do any differently with that than you would FileIsLockedException or NetworkIsDownException or HarddriveIsCorruptException? You wouldn't do anything differently. Show the user the ErrorMessage in the dialog and allow them to restart whatever operation was in affect. Let them choose a shorter path, close Excel, plug their Ethernet cable back in, or whatever.
I don't think this approach works in general. Server software, at least, needs to automatically handle all these different failure modes in potentially unique ways.
If you are aware of particular situation, you can handle it. If you aren't aware of a particular situation at best you log the problem and skip the operation. That's pretty straight forward and is still the same approach.
For example, I might just catch every single Network exception, log it, add a timeout, and retry the operation. I'm not playing wack-a-mole and I don't care in the handler which of the thousands of methods called in my operation actually raised the exception.
The point is one should not be looking at what exceptions the methods throw but instead what exceptions can be handled. One set is very large the other set is very small. This applies equally well to server software.
> No, you shouldn't. What are you going to do any differently with that than you would FileIsLockedException or NetworkIsDownException or HarddriveIsCorruptException? You wouldn't do anything differently. Show the user the ErrorMessage in the dialog and allow them to restart whatever operation was in affect. Let them choose a shorter path, close Excel, plug their Ethernet cable back in, or whatever. Trying to play wack-a-mole is a fools errand.
I don't understand this viewpoint. If a file is locked, the user may be able to fix it, so go ahead and display an error. But if there is corruption, then what is the user going to do? You can't tell them, "Restore your system from backup, and then click OK." Your program has to crash before it causes more problems.
What if the situation is stack corruption or heap corruption? Or what if the error left the application in an inconsistent state? Continuing to run might corrupt the user's data. What if an attacker caused the corruption in order to gain unauthorized access? Logging or displaying the error might be exactly what they want.
I often see variations of this advice: If you don't know how to handle it, don't catch it. By definition, you don't know how to handle an unknown exception.
> Put MSDN away, you don't need it.
I think it is a good idea to understand the behavior and failure modes of every API function that you call; this requires that you consult the documentation.
> But if there is corruption, then what is the user going to do?
Corruption is outside the state of this discussion. If half your app just overwrote the other half of your app, no matter what we say here is going to make any difference.
> Or what if the error left the application in an inconsistent state?
That's a good question. The other part (and in my opinion the important part) of exception handling is ensuring your stack unwinds correctly. Managed languages help this with garbage collection, resource-using statements, and finally clauses. C++ does it with RAII. This is typically just your normal resource cleanup; if you are cleaning up correctly without exceptions you'll cleanup correctly with exceptions.
> I think it is a good idea to understand the behavior and failure modes of every API function that you call; this requires that you consult the documentation.
You are right, of course. You should definitely know the behavior and failure modes of every framework and library you call. And you should consult the documentation for that. But what you don't need to consult (or even necessary know) is the mapping of every method to every exception. You should look at the total set of all exceptions and handle the ones you can handle.
> Corruption is outside the state of this discussion.
Why? If corruption leads to an exception, it seems relevant. Corruption could be the application's own fault, it could be failing hardware, it could be someone trying to exploit a vulnerability. It could be anything - a result of undefined behavior. Sorry if I'm using the term loosely.
> If half your app just overwrote the other half of your app, no matter what we say here is going to make any difference.
If it overwrote the other half, it should crash immediately. I can't just display an error message and retry the operation in that case. If I don't know what exception I'm handling, how can I be sure that I won't cause more damage by handling it?
> The other part (and in my opinion the important part) of exception handling is ensuring your stack unwinds correctly.
That is not what I was referring to. A program can still get into an inconsistent state with exceptions. For example, if a function updates half of some data structure and then throws, that data structure may be left in an inconsistent state. Of course you should not write functions that way, but it is an easy mistake to make. Do you really want your app to keep running in an inconsistent state?
I'm looking at it from a defensive, assume-the-worse perspective. If my application is in an inconsistent state, it should cease running immediately. If my application does not know which state it is in, it must assume that it is in an inconsistent state.
This has nothing to do with exceptions. If some function updates half of some structure and then just returns due to a bug then you have an inconsistent state and no exceptions were involved at all. And your application keeps running in an inconsistent state.
That's my point: in this situation, you can't display an error and keep going. How do you know that you are not in this situation when you catch an unknown exception?
But in the same situation the code might not trigger an exception and continue just the same. You've gained or lost nothing.
You have to assume that code cleans itself up as the stack unwinds. That should be normal operation whether or not you are using exceptions or not. If the exception is in the middle of modifying a data structure then the cleanup should ensure that data structure is either back into a stable state or destroyed completely.
>You are right, of course. You should definitely know the behavior and failure modes of every framework and library you call. And you should consult the documentation for that. But what you don't need to consult (or even necessary know) is the mapping of every method to every exception. You should look at the total set of all exceptions and handle the ones you can handle.
You keep stating this, but it doesn't make sense. By definition, for you to handle it somewhere requires knowing the entry point to the call chain that may throw exception(s) that you decide to handle. At this point, you're wrapping some method invocation with a catch block, are you not? If so, you need to know that this method throws these exceptions. Sure, in some cases you may not care which of the tens of methods in the call chain actually triggered a SocketException, but that's a specific case and doesn't solve the overall problem. What if there are several sockets involved in the operation and you want to reconnect just the failing one, for example? At some point, you're going to have to sink your error handling to the point where you have enough context to handle it, which invariably means it's not enough to know the general types of exceptions an entire framework/library throws.
> By definition, for you to handle it somewhere requires knowing the entry point to the call chain that may throw exception(s) that you decide to handle. At this point, you're wrapping some method invocation with a catch block, are you not?
I think you're still missing the point and maybe I'm not explaining it well. For you to handle an exception, you don't need to know where something may throw an exception. You just need to know where you can handle the error. For example, if I'm iterating a data set and performing a web service call per iteration then I'm more than likely going to put my exception handling inside that iteration around that service call. My recovery is more than likely going to be retrying the service call a few times and then give up and iterate to the next item. Perhaps enough overall failures will terminate the whole loop.
So yes, at some point I will be wrapping some method invocation with a catch block. Does that method throw an exception? Yes. All methods throw exceptions.
> What if there are several sockets involved in the operation and you want to reconnect just the failing one, for example?
Obviously, you do need to put the exception handling code where you can reasonably handle the error. Which is just restating my point. If your code generates several sockets and performs operations on them then the error handling should be connected to those operations. So again the decision about where to put the error handling is not based on which methods throw (they all throw) it's about where it makes the most sense to handle it.
I believe exception handling should build up from the generic to the more specific as needed. If you have to handle every single exception at every call site then that's building it down from the most specific case. Java forces you to do this and it's almost always a waste of time and mental effort. It also makes iterative design difficult. Most first versions of my applications have just one single handler that logs everything and terminates. Many times that's good enough. But where it's not good enough, the next version can be easily improved with specifically targeted exception handling sprinkled in as needed.
I started to think about everything that's wrong with the authors code example and the more I got into it the more I realized the entire problem is wrong.
"Our goal is to write a function that takes a list of previously released versions and a user-provided version string, which then returns the parsed number or reports an error."
A function named CheckNextVersion should never even raise an exception because it expects bad input. It probably also shouldn't double up as the ParseNextVersion function as well. But I understand the author wanting to combine these operations and conveniently get an error message out that can be displayed to the user. But as a damning example of exception handling, it's just bad design.
So even if we accept the design, his method is horribly over-thought with regards to error handling. Either it should catch within it's body all potential exceptions and raise a single InvalidVersionException or it should catch nothing and the code above should catch everything and display the message to the user. All the work catching and rethrowing different exception types adds no value given the purpose of the method.
I have building a toy language and about this I have wondering if is better to think in errors exactly with normal values.
So, my functions are ALL like this:
fun sample= StdOut(Str) | StdErr(Int) do
if GoodLuck() do
return "Yeah!"
fail 1
ie: fail is exactly like return, is just a way to split between Ok values and Err values.
If the client wanna catch them:
result = sample() # Get the Ok value. if the Err value is returned, then trow like in exceptions
result != sample() # Get the Err value, not trow
result, err != sample() #Get both
> I have building a toy language and about this I have wondering if is better to think in errors exactly with normal values.
That's exactly what Rust (to keep with TFA) does. Result is not really special, its core is just
enum Result<T, E> {
Ok(T),
Err(E)
}
> What do you think?
That the code doesn't make sense, the result and the error should be exclusive propositions, why would you get both a result and an error when the function should return either one or the other?
Those are used for IO or user signaling, not error reporting (that's the process's return status, stderr may be used to augment it with a cause, unix utilities are centered about the status signaling correct/incorrect execution).
> I think, a function/call get a value or a exception, so why not be more explicit about that?
It's fine to be explicit about it (again that's what Rust does), the problem is your example isn't really explicit about it, and the second sample is downright weird. It's like Go except even more screwy.
edit: in an other subthread you mention following dynamically typed languages, if we go with that and assume no type support it makes slightly more sense, but the logic is still screwy: assuming the second snippet is a function which doesn't have a result (so it can only have an error) it should use the same system as the third snippet, the faulting assignment (first snippet) is the one which should use a different syntax.
And then there'a the question of what semantics you get when not assigning but just nesting expressions.
An option might be to attach the decision to the call instead (kinda like Swift) e.g. the result of a regular function call would be either `error` or `result, error` (with the language enforcing that one of result and error must be nil) but by "banging" the function the error is raised instead of returned. So you'd get something along the lines of:
# no result, possibly an error
err = foo()
# a result or an error
result, err = foo()
# no result, panics in case of error
!foo()
# a result or panics in case of error
result = !foo()
the language would enforce that either the error fetched or the function is bang-ed, so
foo()
would be illegal, because it neither receives the error nor raises on error.
Yep, as I have think, is more or less like you say.
>if we go with that and assume no type support
Wonder why is harder with a type system (I say follow the syntax, not the type system of julia/python... so I wanna be as static as possible, or at least try)
Ok, was a big mistake to show the idea with this on-the-fly-invented syntax this way (because: I still not settle on it), I just try to convey the idea not the implementation.
I wanna build a F#-like type-system with sum/products (https://www.reddit.com/r/fsharp/comments/30ywjy/question_how...) for my language, and be the result of the function already a Result<T, E>, without the user need to annotate it manually. So, I'm thinking in the kind of development that happened in reactive library but as a intrinsic model of the language.
Of course, I'm a total noob in this area, so still researching about this!
That being said, I don't particularly like the overloading of `!=` there. I like the idea of a shortcut meaning "call this function and get the error value", but I wouldn't use `!=` for it.
Perhaps take a page from Lua's book and use `_` as a throwaway variable. So `a = foo()`, or `a,b = foo()`, or `_,b = foo()`.
I'd love it if some Rust insiders could comment on how Rust might implement the stack trace feature (assuming they did decide to implement it). Perhaps a trait such as "GeneratesStacktrace" that can be implemented with "Derives"? And the stack trace is only available in a Debug build when you compile with the appropriate flags?
I've barely given it any thought, but one approach would just be to modify the try! macro to dump a backtrace to stderr when an Err is encountered in debug mode. The backtrace itself could be generated the same way that they're generated for C++.
Whilst some of the issues with C# are valid, it's totally possible to do monadic error handling in C#, I wrote an entire library to facilitate that [1]. In fact the big win of monadic error handling is the composable nature of monads:
Here's a function that throws an error on odd numbers:
Here the monadic computation succeeds, and 'res' holds 1000:
var res = match(from x in Number(10)
from y in Number(10)
from z in Number(10)
select x * y * z,
Succ: x => x
Fail: 0);
Here the monadic computation fails, and 'res' holds 0:
var res = match(from x in Number(10)
from y in Number(9)
from z in Number(10)
select x * y * z,
Succ: x => x
Fail: 0);
If you don't want to use it in a LINQ expression then you can just say:
var res = Number(10).IfFail(0);
There's also mention of the null problem, which obviously is a major problem with the language (and framework), but it can be tamed somewhat with Option<T> and TryOption<T>:
Here the monadic computation succeeds, and 'res' holds 1000:
var res = match(from x in Number(10)
from y in Number(10)
from z in Number(10)
select x * y * z,
Some: x => x,
None: 0,
Fail: 0);
Here the monadic computation fails, and 'res' holds -1:
var res = match(from x in Number(10)
from y in Number(9)
from z in Number(10)
select x * y * z,
Some: x => x,
None: 0,
Fail: -1);
Here the monadic computation also fails because a None is returned, so 'res' holds 0:
var res = match(from x in Number(10)
from y in Number(20)
from z in Number(10)
select x * y * z,
Some: x => x,
None: 0,
Fail: -1);
So as you can see, using monadic error handling is doable in C# with the right tools. It certainly takes a bit more discipline from the programmer, but it's definitely worth it. It also makes your code declarative. A function that looks like this:
TryOption<string> DoStuff();
... clearly declares that the function may: Succeed, return 'no value', or fail. So you should deal with all 3 possibilities. In contrast, this doesn't give that information to the programmer:
In user code (as opposed to framework code), Typed Exceptions are overrated.
Let's take these two cases:
1. End-User: Doesn't care if the Exception is a ParseException or an InvalidVersionException. Exception("Invalid number") and Exception("Version should be higher") is just as good enough for a message. Basically, for the end-user, the app crashed and that's it.
2. Programmer: Will have to open the debugger or check the code anyway.
I get it that it's useful in some cases, when you wanna send back some structured data to the caller. But in the vast majority of cases, the plain Exception class and Stack Traces suffice.