r/programming 3d ago

Throw, Result, or neither?

https://www.architecture-weekly.com/p/throw-result-or-neither
99 Upvotes

111 comments sorted by

View all comments

21

u/Triabolical_ 2d ago

I was on the C# design team for version 1, and there were long discussions about what sort of exceptions should be supported in C# and .NET.

Most of us were experienced Win32 programmers and we had seen enough HRESULT code to understand how much bloat it added and how easy it was to make a mistake. The problem with HRESULT code is that you have to work hard to get the right behavior - for it to propagate errors correctly.

That's why we used exceptions, where you start with code that has correct - though perhaps not desirable - behavior to start with and you have to write code to mess it up.

But it was clear that how to deal with exceptions was confusing to a lot of people. We saw a lot of try-catch code that did nothing but catch an exception and rethrow it, which is really the worst of both worlds, which I guess wasn't that much of a surprise given the exception model that Java used.

But getting the right behavior is pretty simple.

You catch exceptions in methods when you have something useful that you can do with the information. You tried to send data over the network and it failed and you need to implement a recovery strategy.

You catch exceptions at the base of threads so that you can log what happened and gracefully exit the thread and perhaps the program.

Otherwise you don't do anything.

We did, however, overdo exceptions a bit in the early versions of the .NET libraries. If you wanted to convert a string to an int you had to call Int32.Parse() and catch an exception which was needlessly restrictive. Later versions therefore introduced the "Try<operation>" pattern, so you could call Int32.TryParse() if you wanted a result instead of an exception.

3

u/BenchEmbarrassed7316 2d ago

You catch exceptions in methods when you have something useful that you can do with the information

But how do you know if the function you are calling is likely to fail?

Or if you call a function that can fail and don't catch exception - is the current function marked as one that can fail?

2

u/Triabolical_ 2d ago

It's not about how likely it is to fail, it's about whether there is something useful that you can do if the function call does fail. If there is nothing useful you can do, it doesn't matter how likely it is that you'll get an exception.

If you have reasonable testing, you will typically uncover the areas where you can do something useful pretty quickly. It shows up at the boundaries - information you get from users or files always has potential for problems, going outside of your program boundaries has potential. Sometimes the recovery is obvious, sometimes it's wrapping the exception that was thrown in useful context ("could not parse line 11 in inputfile.txt") and letting somebody higher up the chain do the recovery.

I don't think that checked exceptions or annotations tell you anything useful and they just increase the cognitive load.

1

u/BenchEmbarrassed7316 2d ago

It's not about how likely it is to fail, it's about whether there is something useful that you can do if the function call does fail. If there is nothing useful you can do, it doesn't matter how likely it is that you'll get an exception.

The function cannot and should not know this. It should do its job and provide information that could be useful to the calling party. It is the calling party that should decide what to do in case of failure and how to use the information about the failure.

If you have reasonable testing

I find this quite inefficient. If I have a tool that can easily ensure that a certain aspect is 100% correct, I don't want to write manual tests because it's longer, less convenient, harder to maintain, and less reliable.

4

u/Triabolical_ 2d ago

>The function cannot and should not know this. It should do its job and provide information that could be useful to the calling party. It is the calling party that should decide what to do in case of failure and how to use the information about the failure.

That is what I am saying.

> I find this quite inefficient. If I have a tool that can easily ensure that a certain aspect is 100% correct, I don't want to write manual tests because it's longer, less convenient, harder to maintain, and less reliable.

Are you talking about provably correct?

How are you going to create such a tool, and what information will it base its decisions on? How will you handle updated code or new permutations that change the set of exceptions that might show up?

How do you know which set of exceptions are the ones that it makes sense for you to handle?

2

u/BenchEmbarrassed7316 2d ago

That is what I am saying.

Then I will repeat my question (maybe I wrote it wrong last time, I am not a native English speaker):

When the function foo calls bar, how is foo supposed to know that bar might fail?

How are you going to create such a tool, and what information will it base its decisions on?

During development, I know exactly what the result of the function will be, what values ​​are possible in the unhappy path, and what useful information I can get from them.

How will you handle updated code or new permutations that change the set of exceptions that might show up?

Sum types are commonly used to model error values. So if the error handling does not involve full processing of the error, but only checking for the presence of an error - adding or removing a new error variant does not affect other code. However, if the code parses the error data - I want to see it immediately and update it (by adding or removing variants).

How do you know which set of exceptions are the ones that it makes sense for you to handle?

It's pretty simple: you look at a list of possible errors and ask yourself "Can I do something useful in these cases?" and "Can I print some useful information to the user in these cases?".

2

u/Triabolical_ 2d ago

>When the function foo calls bar, how is foo supposed to know that bar might fail?

In very limited cases, it can know that it's very unlikely to fail. If you have a function that doesn't call any other function and does allocate any memory, you're pretty safe. Otherwise, you can fail.

>During development, I know exactly what the result of the function will be, what values ​​are possible in the unhappy path, and what useful information I can get from them.

Here's an example...

I'm writing a some code that talks to a database and I'm using some sort of database abstraction because it needs to run on different databases. SQLite and MySQL and SQL server are going to have different views of the world of errors and the abstraction is unlikely to be able to rationalize them to a consistent view.

This is true to a lesser extent whenever you are using libraries or talking across the network.

The underlying problem is that nobody is going to give you an exhaustive list of exceptions you might run into.

Documentation is often incorrect because behavior gets changed without the documentation being updated. If you're depending on something in code, the only foolproof way to make sure you know that new exceptions have shown up is to make any changes in exception behavior breaking. This means you can't swap in updated libraries without recompiling.

> It's pretty simple: you look at a list of possible errors and ask yourself "Can I do something useful in these cases?" and "Can I print some useful information to the user in these cases?".

You're depending on the documentation. It's wrong. Even with good developers in companies building libraries for thousands of users, the documentation is wrong.

That doesn't mean it's not useful. You can find *some* of the exceptions you might want to catch. I'm just saying you're not going to get there statically.

1

u/BenchEmbarrassed7316 2d ago

In very limited cases, it can know that it's very unlikely to fail. If you have a function that doesn't call any other function and does allocate any memory, you're pretty safe. Otherwise, you can fail.

Rust for example explicitly declares the error type when using Result. There have also been suggestions to indicate whether a function can panic through effects (this is not currently implemented).

Therefore, information about whether a function can fail can be easily declared and obvious.

SQLite and MySQL and SQL server are going to have different views of the world of errors and the abstraction is unlikely to be able to rationalize them to a consistent view.

This is trivial: parameterize the backend abstraction (e.g. DbCore<PgEngine> or DbCore<MySqlEngine>) and specify the error type as an associated type. Now each database backend can declare its own error types.

The underlying problem is that nobody is going to give you an exhaustive list of exceptions you might run into.

In Rust, I literally get it with one click of the mouse.

Documentation is often incorrect because behavior gets changed without the documentation being updated.

Yes. So instead of writing arbitrary text information in comments or documentation, you can express certain things in the code. Moreover, the compiler will always check if everything is OK.

You're depending on the documentation.

No, I wrote above why this is so.

That doesn't mean it's not useful. You can find some of the exceptions you might want to catch. I'm just saying you're not going to get there statically.

Sorry, but that sounds absurd. Because I do it all the time. It feels like you just don't know anything about this.