Hacker News new | ask | show | jobs
by latch 3521 days ago
My non-node specific suggestions:

1 - Don't catch errors unless you can actually handle them (and chances are, you can't handle them). Let them bubble up to a global handler, where you can have centralized logging. There's a fairly old discussion with Anders Hejlsberg that talks about this in the context of Java's miserable checked exception that I recommend [1]. This is also why, in my mind, Go gets it wrong.

2 - In the context of error handling (and system quality), logging and monitoring are the most important thing you can do. Period. Only log actionable items, else you'll start to ignore your logs. Make sure your errors come accompanied by a date (this can be done in your central error handler, or at ingestion time (via logstash or what have you)

3 - Display generic/canned errors to users...errors can contain sensitive information.

4 - Turn errors you run into and fix into test cases.

[1] - http://www.artima.com/intv/handcuffs.html

6 comments

When it comes to 1.), a better way to state things is may be that you shouldn't ignore errors unless you were able to completely handle them.

Catching exceptions to throw exceptions with better messages is something I would stronly suggest, since almost no exceptions are useful without contextual information. I.e. which file was not found? The config, not the input or output. Things like this. This is especially useful in C++, where you don't get stack traces, but also in other languages you'll want to present non-technical (i.e. non-dev) users with meaningful messages. Stack traces will just frighten them off.

Your comment on sensitive information also plays into this.

I'll agree that just swallowing errors and going on is a recipe for disaster. This is something that regularly bugged me in most of the C code I've encountered so far.

Catching exceptions to throw exceptions with better messages is something I would stronly suggest

Also not every user in the world understand English.

I catch exceptions in two levels. One is the user initiated action. I seldom see this specified, but it seems obvious to me: if the user decides to do X, either X is done or a clear and meaningful message is shown, explaining what and why could not be done.

There is another finer grained location to do what you say (collecting details), logging and then re-raising up to the other level.

Swallowing errors is evil and no, not limited to C code.

Interesting point. I have to confess that I've never seen logging done usable in a i18n sense. Of course, for UI applications, you're absolutely right.

When it comes to logging details, you have a point. But I still think a clear final error message is something to aspire to - especally if you're writing very busy and potentially multithreaded (or - even worse - async) services.

On this note, filtering logs according to thread IDs can be very helpful here. I wonder if that is also easily possible with "fibers".

> Swallowing errors is evil and no, not limited to C code.

Of course, nothing ever is - it's just where I've seen this thing the most (highly anecdotal evidence, I know :-) ). Unfortunately, the nature of many C APIs also makes it very non-obvious if you're skipping through the code, whereas an empty catch statement stands out somewhat.

> Catching exceptions to throw exceptions with better messages is something I would stronly suggest, since almost no exceptions are useful without contextual information.

Much more important is to wrap these exceptions generated in internal code in custom types. List of raised exceptions is a part of code's interface, and one shouldn't generally expose this kind of internal details as public API.

If you're wrapping third party code that uses custom exception types, absolutely. However, regarding custom types, I found that you can get astonishingly far with most standard error types defined by many languages. So, if the internal code already uses those, I wouldn't see them as much of a problem - a FileNotFoundException essentially may as well stay one.

Of course, default exception types won't work if you have to convey more contextual information that is missing in the according interface. Also, using the same default exception type for different underlying errors can be problematic (an example for this would be C#'s AppSettingsReader.GetValue()[1] - it makes it impossible to distinguish between a parsing error or a missing key via the API).

[1]: https://msdn.microsoft.com/de-de/library/system.configuratio...

You can sometimes get stack traces in C++, but it's platform and compiler specific. Gcc's system is pretty good.

(I have some code for WinCE that walks stack traces in conjunction with SEH so that crashes in production - segfault etc - get logged in a useful manner. It does rely on parsing and decoding instructions ...)

> Only log actionable items

Easy to say but much harder to implement. For example, if you communicate with another service, a few network errors are usually not actionable and you'd have some fallback mechanism in you code. But tons of network errors (e.g. > 20%) is a problem that needs to be fixed now. So would you log the network error or not?

Set a threshold, and log only once you hit that threshold.
And keep track of that state across 20 different instances?

What we do is just log the failure and have a system like New Relic monitoring everything so that it can alert us when we hit 20% network failure.

Sure - but then the developer-facing "log" is the New Relic interface, and your instances transmit failure information to it via some API (I mean I suppose you could have one program output a plain-text log file and then another program or service parse that to figure out how many errors were happening, but you wouldn't do that for any other kind of inter-system communication).
Monad transformers offer a more disciplined and pleasant alternative to your #1. You should handle errors at the value level, not with some magic error passing system provided by your language. You can catch the errors at whatever level you like, and you are forced to deal with them properly by construction.
Are you talking about things like EitherT? IMO here isn't so much of a difference to exception handling. Both approaches make it hard to see at a glance where most code could fail, and you can (but are not encouraged to) transform errors explicitly.

Add Java's checked exceptions, now the practical differences are quite subtle. Of course it's nice to be able to be able to do transformations with higher level functions.

Standard "canned" reply: Java doesn't force you to annotate exceptions or even to handle them all. Typed returns do. And as to the "where", well, in the originating function.

But that gets us to some real criticism here: You run the danger of getting an Either Monad out of about every function in your code-base... So maybe throwing exceptions isn't the worst thing after all. (/me ducks all the stuff being thrown my way from hardcore [EDIT:] ~Haskell~ functional programming fans :-))

EDIT: s/Haskell/functional programming/ (because its unfair - Haskell can throw stuff: http://www.randomhacks.net/2007/03/10/haskell-8-ways-to-repo...)

Source? I was under the impression that it does (apart from things like NullPointerException, which of course has Haskell equivalents). And I've had to do a significant project in Java.
You mean Unchecked Exceptions (that extend from RuntimeException) vs Checked Exceptions?
You can manipulate value-level things like EitherT in ways that are not convenient or possible in Java.

EitherT and friends also force you to handle exceptions explicitly before getting a value out. In Java, you can usually just ignore it and "let it bubble up" as someone suggested earlier.

"Manipulate", that's what I meant by "transformations by higher order functions". The point is acknowledged, but I feel in practice it's not always beneficial. There are typically not many more points of use than different transformations. Doing the stuff inline (with catch blocks) is often better since there's less conceptual overhead.

"Bubbling up", that's exactly what the monad instance gives you. How EitherT is supposed to be used. That's why I said there's not much difference from a practical standpoint. So no, EitherT does not force any better style than checked exceptions (but it makes it really inconvenient to traverse regions of code with differing sets of exceptions).

I still feel that the C-style way of handling error codes is superior in most situations, from a writeability and readability perspective. The big problem is it doesn't enforce error handling. Another problem is it's totally unsuited for quick and dirty scripts like I can write with Python "unchecked" exceptions: Just do it, and tell me if there were errors (I might or might know which ones are possible) only at runtime.

For point nr 4, at our company we tracked how many bugs that were fixed, came back afterwards (project with 80 developers for more than a decade old code base). We saw it's very rare that the same bug (after the fix) will pop up twice, and therefore decided not to add or write regression tests on them, and focus our testing efforts where it mattered more.
It depends on what kind of error it is. If it is an off by one error, there isn't much you can do to test it, but if it is an input edge case you didn't handle correctly, then a test make a lot of sense, if for nothing else to verify the fix.
Of course, it is rare that same bug will pop up twice, but bug demonstrates areas which are not covered by tests, so new test(s) will cover these areas and will catch an other bug(s).
> This is also why, in my mind, Go gets it wrong.

Considering the amount of places I see c# code catch errors and continue as though nothing happened I have to wonder how many wild go and c programmers simply ignore error codes?

Once I got used to it I found I kind of like the Go paradigm of checking for errors every time something could go wrong, and (usually) passing the first one up the chain with some additional context info.

However, the fact that "ignore error" is an easy and built-in paradigm that even shows up in the official docs:

    fragileThing, _ := scary.MightNotWork()
that fills me with dread.
Could you elaborate?

Assuming that scary.MightNotWork() is some kind of ancillary function that is non-essential, why would I want to let it impact the main program. The example that comes to mind would be logging. If I have set up my own "Write logs into network share" call, I'd never ever expect it to throw errors that took down the app. Share down? Don't care. logfile locked/corrupt. Don't care. Try and log, if you can't, fail silently without impacting the main purpose of the application/service/whatever.

> Try and log, if you can't, fail silently without impacting the main purpose of the application/service/whatever.

Are you being serious? I agree that there can be "some kind of ancillary function that is non-essential" but in the case of failed logging you should try sending an email / showing some warning if you have a GUI / try other outputs / crash with a meaningful error especially if you are running under some sort of supervisor... etc.

Of course that doesn't invalidate your main point.

Sure. If your function does things internally that might return errors, the normal thing to do is have your function also potentially return an error, namely the first error it finds.

If you call a function the error of which isn't a big deal to your function, you'd normally check the return value to make sure it meets your expectations. If it does, fine -- proceed accordingly. If it doesn't, then send the error up the chain.

So -- totally bogus example -- say you want to return the mod time of a file or, if the file doesn't exist, the epoch. The file not existing is an error, but not one you'd abort on; other file errors though would be problematic:

    // ModOrEpoch returns the modification time of the file at path, or the epoch
    // time if there is no such file.  Unexpected file conditions are returned
    // as errors.
    func ModOrEpoch(path string) (time.Time, error) {

    	epoch := time.Unix(0, 0)
    	info, err := os.Stat(path)
    	if err != nil {
    		if os.IsNotExist(err) {
    			return epoch, nil
    		}
    		return time.Now(),
    			fmt.Errorf("File error for %s: %s", path, err.Error())
    	}
    	if info.IsDir() {
    		return time.Now(),
    			fmt.Errorf("File is a directory: %s", path)
    	}

    	return info.ModTime(), nil

    }
https://play.golang.org/p/AMSJMV3tks

I suppose it's possible you might really, really not care about an error, but that would be extremely un-idiomatic in Go. And, I would argue, a very bad habit in any language where the error could possibly be other than what you expect; sort of like

    try { foobar(); } catch(e) { /* global thermonuclear war? */ }
The thing that bugs me about seeing the errors ignored in official docs is that the Go world puts a lot of emphasis on writing idiomatic code, and the Go "idiom" is much less flexible than, say, Perl's. You might reasonably look to the official documentation to learn what is and isn't idiomatic. And you would hopefully figure out soon enough that ignoring errors isn't, but then again you might not.
> I would argue, a very bad habit in any language where the error could possibly be other than what you expect

Assuming that ModOrEpoch was being used to populate a mouseover somewhere on a ui that literally could not be less important, any error propagation is going to be degrading service considerably more than swallowing any error and returning epoch time.

Unless there is (and there could well be) a subset of errors which actually cause serious concerns but don't cause errors in any other function in the application? Do you have any examples/thoughts of what that might entail. :)

I think I might not be getting my point across here.

I'm not saying you should propagate the error all the way up to the end-user in a UI.

I'm saying that you very, very rarely would be so uninterested in a lower-level error that you wouldn't even want to log it, except for a set of expected errors.

So when we imagine a "subset of errors which actually cause serious concerns," we should be realistic about what "serious" means -- but I say that for the "subset" that is all unexpected errors, the minimum level of "serious" is that you want to know it happened. In my contrived example code, imagine the web server process doesn't have permission to stat the given file. You'd want to knwo about that, right?

If you're not doing the mouseover properly N% of the time, you have a more-or-less serious problem with your UI, and you presumably want to know that's happening and why. Maybe what you do about it is expand your set of known errors. But at least you have the option of doing something about it.

I think that's in the docs mostly for conciseness. I don't think anyone thinks that it's a good practice in real code.
So you admit Go error as value is verbose. Because they are. The best system ihmo is Java's. If a method throws then IT should be part of the method signature and dealing with the error (try/catch) should be mandatory. The irony is that Go has some form of (inferior) try/catch with panic/recover. So it has both but still pretends exceptions are bad.
The problem with checked exceptions is nothing more and nothing less than they didn't work. Yes, in theory, or at least some theories, checked exceptions ought to be awesome. But they aren't in practice. Go's error handling in practice works better than Java checked exceptions. Where fact and theory conflict, fact wins.

(I emphasize "checked" because there is a much more robust and interesting discussion about exceptions in general, and then of course a number of sidecar discussions about other error handling mechanisms like Either/Option, etc. I'm only making this claim about checked exceptions. Which is kinda shooting fish in a barrel; arguably C's error handling worked better than Java's checked exceptions and I think C's "error handling" isn't even worthy of the term.)

If you can't handle an error, why would you allow it. It makes no sense. As a developer you have control of the errors/exceptions that get raised and raising an error that you can't deal with seems.. bad.
I'm not sure if I understand you correctly. What do you mean by "allow it"?

I would even define an error to be a situation the code can't handle properly, and those things are usually not under your control (if I rip out a harddisk while some program is running, the developer can't do anything against that - but I would hope that the program fails accordingly and states why it failed).

I'm obviously paraphrasing here, but things like this do happen (USB devices get disconnected, remote services go down etc).

Edit: Obviously, this applies to different code at different levels. Serialization code might fail due to input - but that also is not under the control of the dev writing the serialization logic. Thus, it should fail.

You "allow" it by doing something that can potentially give an error without handling it there. I'm referring to the parent's recommendation "Don't catch errors unless you can actually handle them" .. and why you wouldn't handle them. You should always handle them. To put it another way, I disagree with the design decision of depending a top level/global exception handler.
Concurrency means you can't prevent errors. Every time you open a file, it could have been deleted out from underneath you, in a race with some other process.

Most files a program opens are not as a result of user action: configuration, libraries, resources, etc. And usually it doesn't make sense to catch these errors at the point of occurrence, because they'll be all over the codebase. And there's very little you can do in response to them.