Hacker News new | ask | show | jobs
by jammycakes 1030 days ago
> External services having gone offline, timeouts, and invalid user input are expected conditions you should handle locally.

Not necessarily. You should only handle expected conditions locally if there is a specific action that you need to take in response to them -- for example, correcting the condition that caused the error, retrying, falling back to an alternative, or cleaning up before reporting failure. Even if you do know what all the different failure modes are, you will only need to do this in a minority of cases, and those will be determined by your user stories, your acceptance criteria, your business priorities and your budgetary constraints. That is what I mean by "expected conditions." Ones that are (or that in theory could be) called out on your Jira tickets or your specification documents.

For anything else, the correct course of action is to assume that your own method is not able to fulfil its contract and to report that particular fact to its caller. Which is what "yeeting exceptions up the call stack" actually does.

> Almost everything else you listed represents a bug in your software that should terminate execution.

Well of course it represents a bug in your software, but you most certainly do not terminate execution altogether. You perform any cleanup that may be necessary, you record an event in your error log, and you show a generic error message to whoever needs to know about it, whether that be the end user or your support team.

Again, what action you need to do in these cases will depend on your user stories, your acceptance criteria, your business priorities and your budgetary constraints. But it is usually done right at the top level of your code in a single location. That is why "yeeting exceptions up the call stack" is appropriate for these cases.

You only terminate execution altogether if your process is so deeply diseased that for it to continue would cause even more damage. For example, memory corruption or failures of safety-critical systems.

> I’m more than a little shocked that you think yeeting exceptions up the call stack is appropriate for these cases.

I hope I've clarified what "yeeting exceptions up the call stack" actually does.

The alternative to "yeeting exceptions up the call stack" when you don't have any specific cleanup or corrective action that you can do is to continue execution regardless. This is almost never the correct thing to do as it means your code is running under assumptions that are incorrect. And that is a recipe for data corruption and all sorts of other nasties.

1 comments

How do you know what to cleanup when you have no idea which APIs might throw, what stack frames might have been skipped when they do throw, and what state was left broken by yeeting a stack-unwinding exception up your call stack?
You clean up processing that your own method is responsible for. For example, rolling back transactions that it has started, deleting temporary files that it has created, closing handles that it has opened, and so on and so forth. You rarely if ever need to know what kind of exception was thrown or why in order to do that.

You can only assume that the methods you have called have left their own work in a consistent state despite having thrown an exception. If they haven't, then they themselves have bugs and the appropriate cleanup code needs to be added there. Or, if it's a third party library, you should file a bug report or pull request with their maintainers.

You don't try to clean up other people's work for them. That would just cause confusion and result in messy, tightly coupled code that is hard to understand and reason about.

Do you write a try block around every call you make? How do you know what calls will throw exceptions and unwind the stack?
Usually, no you don't. You only write a try ... catch or try ... finally block round the entire method body, from the point where you create the resources you may need to clean up to the point where you no longer need them. For example:

    var myFile = File.Open(filename);
    try {
        while ((var s = file.ReadLine()) != null) {
            var entity = ProcessLine(s);
            // do whatever you need to do to entity
        }
    }
    finally {
        myFile.Dispose();
    }
C# gives you the using keyword as syntactic sugar for this:

    using (var myFile = File.Open(filename)) {
        while ((var s = file.ReadLine()) != null) {
            ProcessLine(s);
            // do whatever you need to do to entity
        }
    }
That sounds incredibly ugly and painful.
It isn't in practice. Only a minority of methods actually need it.

It's certainly far, far better than having to add exactly the same check after every method call. Which is only what you need to do if you're working in a situation where exceptions are not an option.

I'll add that C# also has using statements that dispose the object when the current scope exits (including if it exits due to an exception) this significantly cuts down on ugliness .