Live data from Hacker News

Python errors as values: Comparing useful patterns from Rust and Go

inngest.com

41–50 of 77 posts

Re: Python errors as values: Comparing useful patterns from Rust and Go

#41
post #9

Why does the author want to swallow these exceptions? Let them propagate and it's obvious where the issue lies. If you can't handle an exception, don't catch it. > It's impossible to know which line might throw an error without reading the functions themselves... and the functions those functions call... and the functions those functions call. Some thorough engineers may document thrown errors but documentation is un…

Author here! > Why does the author want to swallow these exceptions? Sometimes you want to swallow exceptions and sometimes you don't. The examples in the article may be a little contrived, but there are situations where logging an error and continuing is better because it prevents data loss. > Let them propagate and it's obvious where the issue lies. If they propagate down the stack then you lose context. You also m…

> his is better than try/catch because that doesn't tell you whether an error could happen

It's really not that simple. Don't forget that the concept of exceptions (and more powerful things, like CL's condition system) were invented because of the problems encountered with errors-as-value approaches.

It's a fundamental trade off without a right answer, and people have been arguing about it now for 50+ years without resolution.

Re: Python errors as values: Comparing useful patterns from Rust and Go

#42
I don't understand why people insist that all errors must be handled all the time. It could be my C++ background, but I feel there are two very different errors:

Expected errors - like "user not found" - should use a value instead of exception. In Python, you can use sentinel objects, or tuples, or None.. lots of options really. Occasionally there is a good reason to use exceptions for flow control even for know errors (various hooks come to mind), but this should be pretty rare compared to number of places that can raise an unexpected errors.

Unexpected errors should not be caught at all, except maybe at the very top level (to record them and return 500 to user). The examples in post, where you catch exception and re-raise Exception back are terrible - what's the point of them? There is no extra clarity, just verbosity. I would defect them in any code review.

Coarse-grained error handling is great as long as exceptions are meaningful and stack traces are good, which is the usual case in python. All that matters for unexpected errors is that (1) user sees an error message and (2) the real cause is recorded for later analysis. A single top-level try block does both.

Re: Python errors as values: Comparing useful patterns from Rust and Go

#43
post #11

The most important thing about writing code is that you write it idiomatically for the language it is in. With Python, idiomatic code is known as Pythonic. def rename_user(user_id: str, name: str) -> User | Exception: # Consume the function user = get_user(user_id) if isinstance(user, Exception): return user user.name = name return user This is not Pythonic. Don't do it. Like it or not, Python uses exceptions. How do…

This topic is explicitly about considering alternatives to the current Pythonic way.

And GP is pointing out (correctly) that most of the time being idiomatic has more net gain than any putative advantage of the non-idiomatic proposal. Exceptions to this are extremely rare.

Re: Python errors as values: Comparing useful patterns from Rust and Go

#44

> tuple[User | None, Exception | None] tuple[User, None] | tuple[None, Exception]

That type won't help if you're unpacking like `user, err = get_user(user_id)`. Both values will be nullable and the type-checker won't understand that `user` is not None if `err` is None

Re: Python errors as values: Comparing useful patterns from Rust and Go

#45
I don't agree with this post.

Engineering is about tradeoffs.

There is more advantage in doing the accepted python solution (exceptions) than inventing your own (which you claim to be better, but I personally think is worse). If you are developing in a team, stick to established conventions and spend your time focusing on your business problem.

Re: Python errors as values: Comparing useful patterns from Rust and Go

#46
post #11

The most important thing about writing code is that you write it idiomatically for the language it is in. With Python, idiomatic code is known as Pythonic. def rename_user(user_id: str, name: str) -> User | Exception: # Consume the function user = get_user(user_id) if isinstance(user, Exception): return user user.name = name return user This is not Pythonic. Don't do it. Like it or not, Python uses exceptions. How do…

This topic is explicitly about considering alternatives to the current Pythonic way.

The topic is explicitly about writing an SDK for the company's product, which is presumably intended to be imported in customers' code and used alongside hundreds of other libraries.

As such, writing idiomatic code for the language of the SDK really should be a concern, rather than implementing some idiosyncratic half-baked version of Go or Rust error handling in Python.

Re: Python errors as values: Comparing useful patterns from Rust and Go

#47
post #40

Quite apart from the Python discussion, the author captures why I prefer errors as values (a la Go) to exceptions (a la Java), and I have written both styles for many years. > Regardless of the specific approach, returning errors as values makes us consider all of the places an error could occur. The error scenarios become self-documenting and more thoroughly handled. This is so true. Most Java exception handling is…

I think it's kinda the opposite actually... with go, you have to go out of the way to generate those errors, while in Python, most of this is automatic.

In particular, the ReadFile example in Python will raise OSError, and those already include filename and error message. So you'd get the same result with 0 extra lines.

For the second example, the json unmarshalling will not auto-add filename, but its easy enough to do using exception chaining:

    try:
        data = json.loads(bytes)
        process_1(data)
        process_2(data)
    except:
        raise Exception(f"Error while parsing file {filename}")
which will give stacktrace like:

    json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
    
    During handling of the above exception, another exception occurred:
    
    Traceback (most recent call last):
      File "somefile.py", line 4, in somefile
    Exception: Failed while parsing file.json
Note that's even more detailed than go, with significantly less boilerplate (only 3 lines per function) vs 3 lines per call.

(an anecdote: we've had the team which converted the CLI tool from Python to Golang. The first time they ran the tool, it printed a single line:

    invalid character '"' after top-level value
and that's it. It took a lot of debugging before they could figure out what happened. All because they got used to python doing super-rich traceback automatically, with 0 effort from programmers)

Re: Python errors as values: Comparing useful patterns from Rust and Go

#48

Earlier quoted context omitted.

> Exceptions are just spooky GOTO and a distance I couldn't agree more! When you throw exceptions it's unclear where the control flow will go

I just don't see how multiple levels of if err: return nil, err in the call stack makes things any clearer? Or how having many instances of that snippet scattered everywhere makes the code easier to read?

Agreed. It's also objectively worse because you lose the stack trace in the final exception you will display to your user. Enjoy debugging that when a customer sends you an error.

Re: Python errors as values: Comparing useful patterns from Rust and Go

#49

One problem I’ve experienced doing something like this is you end up with both exceptions and error values since the standard library and 3rd party libraries are still primarily exception based. You either have to live with it or create wrappers that catch errors and return them as values.

We write wrappers for standard and third-party libraries that throw errors. For example, we have our own dump_json function that catches-and-returns errors thrown by json.dumps. We didn't need many wrappers given the nature of our SDK, but some programs will need many wrappers and that could get unwieldy

But people are going to import your SDK, so your customers will be stuck with a mix of thrown exceptions and errors-as-values in their code.

Based on your decision to write wrappers internally, presumably you'd argue that the sensible thing for them to do is to write wrappers either for your SDK or for every other library to get back to a consistent style and less mental overhead.

Re: Python errors as values: Comparing useful patterns from Rust and Go

#50

Earlier quoted context omitted.

> If they propagate down the stack then you lose context. You also may not have all the data you need to properly recover (e.g. still write to a table but make the errored field null). Is this not solved by using raise from?

By "context" I mean other data that lived at the time of the error. For example, you may want to update a table row when there's an error but if a thrown error takes you too far down the stack then you might not know the row ID anymore

you would have that preserved as a variable in the context of the error no?
Post reply on HN