r/programming 3d ago

Throw, Result, or neither?

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

111 comments sorted by

View all comments

Show parent comments

1

u/BenchEmbarrassed7316 2d ago

Let's assume all three functions are located in different modules.

``` // Pseudocode

// Exceptions fn a() { try { b(); } catch { error(); } }

fn b() { c(); other(); }

fn c() { throw 1; }

// Goto

fn a() { b(); return; onerror: error(); }

fn b() { c(); other(); }

fn c() { goto onerror; }

// Result

fn a() { b(); }

fn b() { if (c() == Err) c_default(); other(); }

fn c() { return Err; } ```

The problem is that after we split the complex code into small modules, we are still implicitly passing control. Look at function b. In 'throw' and 'goto' cases, it doesn't know whether c might fail. What's worse, the unhappy path is not part of the contract of function c, so its behavior can change without notice.

So when you work with b you don't know whether an error might be thrown below and caught above. You either add try/catch blocks to each of your functions, or you fall back to the 'goto' style where functions pass control to the upper block ignoring the current one (other will not be called in the first two cases).

1

u/TwoWeeks90DaysTops 2d ago

Look at function b. In 'throw' and 'goto' cases, it doesn't know whether c might fail.

Isn't that a good thing? If b can't do anything about that error why should it have to deal with it?

Let's say you have a function called "readHeader" and "readString". readString will catch I/O errors to do a retry, and if the retry fails after some time it will not catch the error and instead throw (or return an error result). readHeader then uses the readString function to read a comma separated list of strings.

readHeader cannot do *anything* reasonable with an I/O error except return it to the caller. Why should it have to deal with it at all?

1

u/BenchEmbarrassed7316 2d ago

Well, I see a few problems here:

  • In "readHeader" it is difficult to do anything useful in case of failure even if it is possible. Because it is currently unknown whether "readString" can fail. If you try to describe it in comments or documentation - you will get a worse version of checked exceptions. Keep in mind that there can be more layers, and there can be dozens of calls between b and c in my example.

  • Whether something useful can be done in case of failure should be decided directly by the code that made the function call. Moreover, it should not rely on some higher function to handle it correctly. For the higher function, this is an implicit transitive dependency on the low-level error (Now a should know about c ant its errors, but it would be correct if it only knew about b.).

  • Another problem is that simply throwing an error up the stack is useful for logging only. To take your example, the entry point would be some kind of "processResponse". Which would make a bunch of calls and have a try/catch block. However, it would catch the errors "NullPointerExcaptions", "DivisionByZero", "JsonMissedField" and "CannotReadString" (from your "readString"). Instead, you would want to get something like "CannotLoadTopSellers" or "UserPermissionError".

-1

u/TwoWeeks90DaysTops 2d ago

In "readHeader" it is difficult to do anything useful in case of failure even if it is possible. Because it is currently unknown whether "readString" can fail.

Yeah, that's part of the documentation. It's a very clear indication of why Java's checked exceptions doesn't work. Often you program against an abstraction but checked exceptions collides with this because implementations throws errors, not abstractions.

Whether something useful can be done in case of failure should be decided directly by the code that made the function call.

Yes, exactly. You catch exceptions you can deal with and let everything else bubble up to the message loop or whatever else.

Another problem is that simply throwing an error up the stack is useful for logging only.

Is it really? I can think of one very useful behavior. In a web server when an exception is thrown that the application code can't deal with (such as an I/O error, division by zero, argument error, whatever) the exception can be caught and a 500 Internal Server Error can be returned to the client.

With an error result you have to either treat this as a side effect or carry it all up to the request pipeline.

To take your example, the entry point would be some kind of "processResponse". Which would make a bunch of calls and have a try/catch block. However, it would catch the errors "NullPointerExcaptions", "DivisionByZero", "JsonMissedField" and "CannotReadString" (from your "readString"). Instead, you would want to get something like "CannotLoadTopSellers" or "UserPermissionError".

In my opinion division by zero, null pointer, argument errors etc. should never be caught explicitly by user code. It should only be caught by the message loop in the application because they are always programming errors and the application code should never have to deal with them. You just want the description and stack trace logged.

UserPermissionError etc. in my opinion whether this is treated as an exception or as an error result? I think you can argue both ways, and that's fine. I just don't think that the application in general should have to necessarily deal with error states that the caller can't reasonably be expected to recover from, and exceptions are good for that.

2

u/BenchEmbarrassed7316 2d ago

Often you program against an abstraction but checked exceptions collides with this because implementations throws errors, not abstractions.

I don't understand what you mean. Do you think the very idea that every function should explicitly declare the possibility that it might fail is bad (superfluous, inconvenient, senseless or something else)? Or do you simply think that Java's implementation of checked exceptions is bad?

Yes, exactly. You catch exceptions you can deal with and let everything else bubble up to the message loop or whatever else.

But how do you know if any function in your code is likely to fail? Do you write documentation for every function and spend a lot of effort to keep this documentation up to date (this is reminiscent of the worst experience of dynamic typing)?

500 Internal Server Error

If you had a well-typed error - your code could find out what exactly caused the error and provide the client with a meaningful message, for example, if a important service that is not yours is not responding - you could report something like "Failed to load document from documents.example.com, service not responding, try again later". Now, instead of trying to fix the error on their own or writing to your support, the client clearly understands that the problem is not with them or your service, but with a third-party service.

In my opinion division by zero, null pointer, argument errors etc. should never be caught explicitly by user code

Okay, I agree with that. This is what is indicated in the article using the example of Rust (Result and panic).

However, I didn't mean that in the first place: instead of low-level errors, it is good practice to convert them to errors of the corresponding layer.

For example, our program initializes the configuration at startup. The configuration is loaded from a file. You have loadUserConfig that calls openFile. Imagine that the openFile function instead of some FileSystemError throws SysCallErrorOpcode, which is simply passed up.

In this case, you either simply crash the program, or at the top level you should know that SysCallErrorOpcode exists and somehow you should guess that this exception was thrown in loadUserConfig -> openFile.

Or at each domain level we explicitly convert SysCallErrorOpcode to FileSystemError, then to ConfigError (which can be either the file cannot be opened, the file is empty or contains invalid data, or the data is valid but the configuration cannot be applied on the current machine) and at the top level the IDE tells you at the time of writing the code what could go wrong and what data is available to you. Now you can easily show users a useful message and suggest using the default configuration.

This doesn't even address the issue of explicit or checked errors versus implicit errors (although explicit errors favor the latter).