> It's too easy to call functions without being aware of the set of possible errors.
But that's entirely fine! Programmers are far too obsessed with exactly which functions trigger which errors when it absolutely doesn't matter. All you need to know is what errors you can handle and where in the code you can handle them.
If there is a network exception and can recover and retry it at the start of the operation, it literally doesn't matter which of the thousands of functions up the call stack could have possibly triggered it. The only thing you need to know is the top-level network exception and where your network processing code starts. And if you can't handle a network error, it literally doesn't matter if one was triggered. The best you can do is abort and record a really good stack trace for that type of error.
> All you need to know is what errors you can handle and where in the code you can handle them.
Surely also what errors can occur. Can the network using library throw disk IO errors? Permissions errors? Maybe it handles all the network errors internally to the library and I don't need to deal with those at all.
How do you handle those other errors though? If there is a disk I/O error, you're basically done. Permission errors, same thing. You report, abort, maybe retry.
You don't really need to know in the specific what kinds of errors can occur. If it's possible to recover from an exceptional situation, it's only useful to know if that situation is possible so you can avoid writing code you don't need to. But there wouldn't be any harm in writing an exception handler for an exception that can't happen except for that wasted effort.
Well it depends on what I'm doing and why the errors might be thrown right? Is it something I can let the user retry if they know what's happening (e.g. IO error because the output folder doesn't exist)? Whether I retry on network errors can depend on what the error is - if the end service is responding saying my call is invalid for certain reasons there can be a good case to just die immediately rather than slowly backing off trying repeatedly in vain.
The flip side of this is I shouldn't need to worry about exceptions that cannot be thrown. When you say all you need to know is what you can handle and where, that list must be a subset of the possible list of things that can be thrown. There's no point worrying about whether I should be retrying something due to network faults if it never uses the network to begin with.
I think of this way; there are broad categories of exceptions that you can handle and specific exceptions. But those exceptions are significantly smaller than the set of all possible exceptions my code (and the framework code) can trigger. I shouldn't have to worry about every possible exception, just ones I can handle. Checked exceptions/errors means you have to deal with the minutiae.
For your example if retrying network, I prefer to simply have a "ShouldRetry" property/interface on the exception itself since the triggering code has the best knowledge of how it should be handled. No need to know every possible network exception and sort them into retry or not retry.
> Is it something I can let the user retry if they know what's happening (e.g. IO error because the output folder doesn't exist)?
My favorite error handling is when you can just put a single handler at the event-loop of a UI based project. On exception you just show them the message and continue running. The stack unwinding code ensures the application maintains correct state and that the operation is unwound. If the user clicked "save" and a failure occurred they get the message and can retry if they want.
This [1] article by Raymond Chen is one of my favorite on exceptions.
I admit there is an elegance to exceptions, but when I'm trying to write reliable code, I find reasoning about exceptions significantly increases my cognitive load. Error handling is a place where a little more verbosity is okay because it keeps my focus on the local context rather than having to consider the entire call stack. I basically agree with the TL;DR of the article: "My point is that exceptions are too hard and I'm not smart enough to handle them".
That's the mistake with exceptions; if you assume every method can throw any kind of exception it actually reduces your cognitive load. Now you just have to worry about what you can handle in the 2 or 3 places in your code you can actually handle exceptions. Instead of infinite number of places an error can occur and the huge number of possible errors.
The idea that error states are perfectly knowable everywhere in the code is a myth. Even if that were possible at one moment, the instant anyone changes code anywhere it will immediately be wrong.
> Now you just have to worry about what you can handle in the 2 or 3 places in your code you can actually handle exceptions
This is only true if your application has no state or invariants that could possibly be invalidated in the face of exceptions. For instance, what would be your solution to the 'NotifyIcon' example given in the linked article?
> The idea that error states are perfectly knowable everywhere in the code is a myth. Even if that were possible at one moment, the instant anyone changes code anywhere it will immediately be wrong
This applies equally to both error codes and exceptions. If a method N layers down in your call stack changes its behavior, that's a potential breaking change regardless of your choice of error handling.
> For instance, what would be your solution to the 'NotifyIcon' example given in the linked article?
The notify icon code is poorly structured to begin with. Simply creating a NotifyIcon object adds it to the UI? That's an awful design. If there was an add to UI step then it would be a non-issue; the half-constructed NotifyIcon object would never get added to the UI. This issue is not magically resolved by having to explicitly handle every error; you can make the same mistake with twice as much code.
> This applies equally to both error codes and exceptions. If a method N layers down in your call stack changes its behavior, that's a potential breaking change regardless of your choice of error handling.
I'm not talking about changing behavior, I'm talking about changing implementation. Behavior is part of the contract. But being able to safely change implementation is the fundamental principle of abstraction and is the basis for polyphorphism. If a method today does a calculation using a database but tomorrow is refactored to use a webservice -- as long as the contract/behavior is unchanged -- then the rest of the code shouldn't have to know about it.
But that's entirely fine! Programmers are far too obsessed with exactly which functions trigger which errors when it absolutely doesn't matter. All you need to know is what errors you can handle and where in the code you can handle them.
If there is a network exception and can recover and retry it at the start of the operation, it literally doesn't matter which of the thousands of functions up the call stack could have possibly triggered it. The only thing you need to know is the top-level network exception and where your network processing code starts. And if you can't handle a network error, it literally doesn't matter if one was triggered. The best you can do is abort and record a really good stack trace for that type of error.