Live data from Hacker News

How “let it fail” leads to simpler code

yiming.dev

21–30 of 165 posts

Re: How “let it fail” leads to simpler code

#21

I'd be very interested to see non-BEAM approaches to enabling this - i kind of end up in the same pattern thanks to "expected? Return an Error . Unexpected? Throw." However, the supervising part is then difficult. How do people approach this in Python? NodeJS? Rust? .NET?

Its common for me in PHP land to call die("Error! $e"); when something really bad has happened in a request and I really don't want anything to happen

That's not without its drawbacks however, especially when you consider tools like swoole and laravel octane that keep your application in a long running process, so if you use die you actually kill one of the server workers instead of just terminating the execution of the request

Re: How “let it fail” leads to simpler code

#22
post #8
post #5

I've seen a lot of new developers shocked by this approach, which surprises me a little. They seem to think that it's up to the application to handle all errors, even those of the programmer(s). This, of course, is unreasonable since it would essentially require knowing all the bugs in advance. :-)

Well, there's software that can cause some degree of harm. For example through servos controlling something physical. While you still probably can't catch all of the issues, you damn better try as hard as you can within reason. I'd also wish for similar rigor from people developing whatever filesystens my data is on. :-) Fail fast is generally a good idea, if you can do it safely.

I think the idea is that there are error recovery semantics that:

1. Determine the last sane state of the system, and work forward from there. (Read the servo position and try to go from there)

2. Have a the "recovery" routine to reset the system. (Take all positions to "zero")

3. Just stop. (Yes, I know this can be bad). And ask a human for help.

Re: How “let it fail” leads to simpler code

#23

I don't agree with this approach. Say you have a network service that relies on other network services. It is not difficult to write those such that they know to back off / retry when something disappears. It's extremely useful in a lot of situations: if you do work on a laptop that gets regularly unplugged, having running test services that know to reconnect makes your life easier. In production, having things autom…

What you're describing are "known" states; the idea behind "let it fail" is that you shouldn't write code that exhaustively handles every single potential outcome, just the ones that are part of your code's path in general use.

Definitely write code to handle network issues. Don't write code to handle random bitflips, ways to handle garbage coming back from the service you're connecting to, or try to handle OOM errors. Just let those fail.

Do not catch everything you can. That's the whole point of "let it fail". An app crashing is totally fine and expected behavior, in a lot of cases (of course if it's not fine, e.g. someone dies, don't do that but if you're working on that kind of software and taking advice from me, you're super duper screwed).

Re: How “let it fail” leads to simpler code

#24
post #14

For all the hate that Java tends to get, the language natively supports this distinction between: * Expected errors - Checked Exceptions * Unexpected errors - Unchecked Exceptions Idiomatic Java also makes heavy use of asserts, e.g. using the Guava Preconditions library.

The problem is that while Java the language itself does support that distinction, a lot of built-in stuff really messes it up. For example, exceptions from closing a file are unexpected, but are an IOException which is checked anyway. Also, even the support that is in the language isn't first-class; e.g., lack of exception polymorphism.

I think that's a symptom of the fact that the distinction is really artificial at the language level anyway. Whether something is expected or not is a function of the requirements. Even OutOfMemory can be expected and handled in certain types of applications (esp. since it gets thrown for things like file handles rather than true memory). And then there are all kinds of cases where routine exceptions like file not found are in fact, unexpected errors (as discussed in TFA).

Perhaps some sort of language level solution could have been found (eg: have explicit interfaces to mark exceptions as expected or unexpected and then exceptions are assigned that using generics or something), but that ship has sailed long ago.

Re: How “let it fail” leads to simpler code

#25

I don't agree with this approach. Say you have a network service that relies on other network services. It is not difficult to write those such that they know to back off / retry when something disappears. It's extremely useful in a lot of situations: if you do work on a laptop that gets regularly unplugged, having running test services that know to reconnect makes your life easier. In production, having things autom…

I've said this often specifically in the context of golang, but while you're right that retries and similar are a common case, they are fairly similar to the 'expected' error case in the article, and can almost always be handled at precisely the place where you raise the error.

In python this is

    @retry.retry(exceptions=[RpcException], tries=5, backoff=2, jitter=1)
    def my_external_rpc_call(...):
        ....
And RpcException will only be raised beyond this if the backend is unreachable for ~30 seconds.

Similarly, rpc services can abstract over this entirely, grpc (and presumably others) allow you to configure the retry policy per rpc service or method, and have it reflected everywhere that is used, without writing wrappers[0].

Which really all is to say, once you have solid libraries that handle retries of operations that are known to be error prone (file IO, network IO, things that could lock/block, etc.) you pretty quickly get into "any error implies we're totally boned".

[0]: https://github.com/grpc/grpc-go/blob/f601dfac73c9/examples/f...

Re: How “let it fail” leads to simpler code

#26

I think that the "let it fail" approach is often inevitable, even when we try to use Result . Often, we see an "unknown" variant in the error enum, as a catch-all for a library's unexpected errors. Then, anyone who calls them must also have an "unknown" enum. And anyone who calls them, and so on. In the end, this "unknown" variant is similar to a panic, in that there's very few reasonable reactions to it: Log it, can…

While everything you said is correct, there are still significant advantages to the 'result' method.

Sometimes you want to return 200 even if most of the backends fail. Sometimes one part may want to retry based on any error.

Even aside from this, disallowing exceptions leads to a very predictable control flow, and makes program state able to be expressed in the type system, which is useful for many reasons on it's own.

While yes, it's often just like an exception or panic, I'll take that over exceptions in my code any day

Re: How “let it fail” leads to simpler code

#27
post #8
post #5

I've seen a lot of new developers shocked by this approach, which surprises me a little. They seem to think that it's up to the application to handle all errors, even those of the programmer(s). This, of course, is unreasonable since it would essentially require knowing all the bugs in advance. :-)

Well, there's software that can cause some degree of harm. For example through servos controlling something physical. While you still probably can't catch all of the issues, you damn better try as hard as you can within reason. I'd also wish for similar rigor from people developing whatever filesystens my data is on. :-) Fail fast is generally a good idea, if you can do it safely.

If you can't fail safely, you better review your entire architecture.

Software fails, you can make failures rarer, but you can't make they go away. You have to deal with it, it's not an option.

Re: How “let it fail” leads to simpler code

#30
post #24

Earlier quoted context omitted.

The problem is that while Java the language itself does support that distinction, a lot of built-in stuff really messes it up. For example, exceptions from closing a file are unexpected, but are an IOException which is checked anyway. Also, even the support that is in the language isn't first-class; e.g., lack of exception polymorphism.

I think that's a symptom of the fact that the distinction is really artificial at the language level anyway. Whether something is expected or not is a function of the requirements. Even OutOfMemory can be expected and handled in certain types of applications (esp. since it gets thrown for things like file handles rather than true memory). And then there are all kinds of cases where routine exceptions like file not fo…

This is right, therefore, in most cases, a library should throw a checked exception, and the caller should decide whether it is an expected error and either handle it or rethrow it, or it is unexpected and rethrow a RuntimeException.
Post reply on HN