Hacker News new | ask | show | jobs
by xlii 1570 days ago
I’m disappointed. I expected some obscure edgecase (like “Main is usually a function…” [1]) but instead that’s about scope handling, contract design and responsibility shift.

“Hello world” method simply calls an API to a text interface. It uses simple call, to a simple interface that is expected to be ever present. I don’t find any bug there. It won’t work if such interface isn’t available, is blocked or doesn’t exist. It won’t work on my coffee grinder nor on my screwdriver. It won’t work on my Arduino because there is no text interface neither.

Of course, one could argue that user might expect you to handle that error. That’s all about contracts and expectation. How should I deal with that? Is the “Hello world” message such important that the highest escalated scenario should be painted on the sky? I can imagine an awkward social game where we throw each other obscure challenges and call it a bug.

It’s nitpicking that even such simple code might fail and I get it. It will also fail on OOM, faulty hardware or if number of the processes on the machine hit the limit. Maybe some joker replaced bindings and it went straight to 3D printer which is out of material? _My expectations_ were higher based on the title.

Now allow me to excuse myself, I need to write an e-mail to my keyboard manufacturer because it seems like it has a bug which prevents it from working when slightly covered in liquid coffee.

[1]: http://jroweboy.github.io/c/asm/2015/01/26/when-is-main-not-...

7 comments

I also had higher expectations after reading the title and was disappointed when I realized it was about failure to handle all possible system call results. I thought it was gonna be a bug in the C standard library or something.

I still agree with the author though. This is a serious matter and it seems most of the time the vast amount of complexity that exists in seemingly simple functionality is ignored.

Hello world is not "simply" calling a text interface API. It is asking the operating system to write data somewhere. I/O is exactly where "simple" programs meet the real world where useful things happen and it's also where things often get ugly.

Here's all the stuff people need to think about in order to handle the many possible results of a single write system call on Linux:

  long result = write(1, "Hello", sizeof("Hello") - 1);

  switch (result) {
    case -EAGAIN:
      /* Occurs only if opened with O_NONBLOCK. */
      break;
    case -EWOULDBLOCK:
      /* Occurs only if opened with O_NONBLOCK. */
      break;
    case -EBADF:       
      /* File descriptor is invalid or wasn't opened for writing. */
      break;
    case -EDQUOT:
      /* User's disk quota reached. */
      break;
    case -EFAULT:
      /* Buffer points outside accessible address space. */
      break;
    case -EFBIG:
      /* Maximum file size reached. */
      break;
    case -EINTR:
      /* Write interrupted by signal before writing. */
      break;
    case -EINVAL:
      /* File descriptor unsuitable for writing. */
      break;
    case -EIO:
      /* General output error. */
      break;
    case -ENOSPC:
      /* No space available on device. */
      break;
    case -EPERM:
      /* File seal prevented the file from being written. */
      break;
    case -EPIPE:
      /* The pipe or socket being written to was closed. */
      break;
  }
Some of these are unlikely. Some of these are irrelevant. Some of these are very important. Virtually all of them seem to be routinely ignored, especially in text APIs.
And specifically no space left on device is a very common error that is also very commonly handled badly. Happened to me yesterday and the error messages I got were unhelpful or non-existent. In Firefox part of a website I was desperately trying to use just stopped reacting for some functionality. Developer tools opened as a blank space. Importing a calendar entry in Evolution produced an inscrutable SQLite error. Starting Chromium (as backup browser in the hopes that the website would work better there) via Gnome did not open any window or show any error. It was only when I tried to start Chromium via the console that I saw a helpful error message for the first time.

Also I always start to mildly panic in such cases, as lots of software corrupts its on-disk state more when the hard drive is full than any segfault, OOM-kill or hard shutdown is able to. I can understand and empathize on how this happens from a software development perspective, but objectively speaking "our entire field is bad at what we do, and if you rely on us, everybody will die". ( https://xkcd.com/2030/ )

Userspace should not expect that any given syscall can only return some set of known errno values. You should enumerate the cases where you want to do some kind of special handling (with EINTR being somewhat more important that other cases) and have path to somehow handle even unexpected errno values.

Both Linux man pages and SUS specify some set of possible error situations, but not all of them. In the man pages case the set is not at all fixed and is subject to change and often does not contain some of the more obscure error states. The SUS "Errors" section are explicitly not meant to be complete and the OS can return additional errno values, additionally the OS can even handle some of the error cases as undefined behavior and not return any error code at all (notable example: doing anything to already joined pthread_t on linux, whish is undefined and does not return -ESRCH).

You're right. The manual contains this ominous notice at the very end of the errors section:

https://man7.org/linux/man-pages/man2/write.2.html

> Other errors may occur, depending on the object connected to fd.

I don't understand why every possible result isn't explicitly documented. This is the Linux system call interface, we need to know everything that could happen when we make these calls.

The right assumption is that every syscall can return any defined errno value. In practice this means that you should handle the cases that you have to handle (-EINTR and for write(2) incomplete writes, which are typical reason for “fatal error: Success”), that you can somehow handle (things like retries for -ENOSPC) and log strerror(3) result for anything that you don't expect (whether you shoult then abort(), exit() or continue depends on how critical the failed syscall was).
The bug is not that the program failed, it's that the program failed but reported a success.
Failure isn't defined by programmed control structures, it's defined by requirements and implemented via programming.

If the requirements of a hello world program include accounting for all error boundaries of the host system, then I am yet to see them written down but would invite anyone to provide them.

The parent comment has made a start in this regard.

every program is given the 3 stdio channels: stdin, stdout, stderr. if the program is unable to use any of these as expected, it's an error that should be reported back to the user. today it's /dev/full but tomorrow it could be a log file with wrong access perms. you don't want to be returning 2 weeks later to find out that nothing was written, and your program didn't complain.
Agreed, the log file example is quite good, but also something like a systemd script reporting success when it actually failed.
This is true of *nix but on Windows they can be absent. In face this is the default for GUI applications.
This particular category of bug is something people - those making up the requirements too - probably wouldn't even consider.
Agreed. In a real world business requirements scenario, it would be dropped as premature optimisation if it was even considered at all.
And this is the reason unit tests are very often insufficient and provide vanishingly small value. They test programming details, while integration tests test requirements implemented via programming. Loved your first paragraph.
To quote one of my favorite books:

  > [Hello, world] is the big hurdle. To leap over it you have to be able to
  > create the program text somewhere, compile it successfully, load it, run
  > it, and find out where your output went.
Those are the goals of "Hello, world!". Create the program, compile it, load it, run it, and find the output. Things that are not goals of "Hello, world!" are handling user input, reusable components (functions), network access, etc etc etc, error handling.

It's fine that the error is not handles, just as it is fine that the output went to stdout. Error handling was not a goal of the program.

> you have to be able to create the program text somewhere, compile it successfully, load it, run it, and find out where your output went.

Note that Rust "cheats" for you here, if you ask Cargo to make you a new Rust project then by default the project it gives you will perform Hello, World correctly when you "cargo run". It will also be version controlled (if Cargo can't figure out what type of version control you prefer, but git is installed, you get a git repo).

Rust's Hello World also of course panics if given a full output device. Because just ignoring errors by default, while very C, is not a good idea and in Rust it's much easier to respond to unexpected errors by just panicking rather than ignoring them.

> Those are the goals of "Hello, world!". Create the program, compile it, load it, run it, and find the output.

(Emphasis mine)

How are you going to find the output if there is an error outputting it and you're not capturing that?

Given the requirements you've given, that would absolutely make error handling mandatory in my opinion.

Interesting perspective, with which I happen to disagree. But I appreciate considering it, thank you.
I guess the argument is that the non-error-checking version fails at the "find out where your output went" stage. hello.c gives the impression that your output went to the file, even when it didn't.

Without a spec, I think it would be harsh to claim hello.c is wrong. But handling the error—in this case, returning it from main to the shell via an exit code—is definitely more correct.

I think they are arguing that it didn’t fail, it did everything you asked of it (it didn’t claim to successfully print hello world in every scenario, just to attempt to write to the buffer you gave it, which it did).
It seems possible they are arguing:

    _exit(write(1,"Hello World!\n",13));
is the correct one, but to me that just kicks the can. What should happen here?

    os.rename(x,y)
    print("success!")
should this exit nonzero? The file did get renamed and progress was made, even if some unrelated problem occurs, so some animation for a users benefit who is probably dealing with some other problems thinking piping to /dev/full was a good idea in the first place, well, it just seems almost cruel to further burden them with a surprising error code, so maybe I should wrap that print line in a pokemon since the output doesn't really matter that much anyway.

So it is I prefer to think of bugs as the difference between expectation and reality, and I think it should be fair to say different users can be predisposed to have different expectations; So I also I think it matters a great deal what the contract/expectations are.

But I also know the difference between /dev/full and /dev/null

I think

   return printf("Hello, world!\n") < 0;
would suffice.

Edit : bugs are easy

Well, if you want your hello world program to try to write Hello world, then report success regardless of the result, then it is bug-free. If you intend your program to write hello world on your screen/stdout, then it is definitely buggy.

The computer will do what you ask it to do, it's only a bug, when it doesn't meet your expectations.

The program failed to check the return value of the called function.
No, I think the argument was pretty clearly that "the program failed but it can't possibly be expected to work under every conceivable scenario".
Whether the program fails or not, is a matter of specification.

    printf("Hello, World!\n")
Is me saying: "Do a write syscall to stdout. I don't care what the return value is, I don't care if the flush is successfull if stdout happens to be buffered." If that is what I want to do, aka. what the program is specified to do, then it didn't fail.
To me, your example says: "I forgot about the return value." The way I learned C, if you really, really want to say "I don't care what the return value is" you'd explicitly cast it, nicely documenting your active non-caring about the return value for future code readers:

    (void) printf("Hello, World!\n");
Although, in general, ignoring the return value from things like puts() and printf() is a bad idea, for reasons the article makes clear.
And how do I explicitly ignore the other ways C communicates error conditions, like global error variables (eg. errno), inbound error values, errflags in structs?
I found it interesting. If you generalize a bit, the question is "Will a naively written stdio program handle IO errors?". The fact that for several popular languages the answer is "no" is disappointing.
To me it was less disappointing and more intriguing.
It’s not about handling the error, it’s about propagating unexpected error. Because most errors are that; unexpected.

Modern languages do this by default, using exceptions, or force you to check return values using Result<> or alike.

Even in C, when compiled through some more strict linter, this would fail because ignored return value should be prefixed with (void).

In either case I think the main takeaway from the article is that a language where even hello world has such pitfalls, isn’t suitable, given the many other better options today.

My initial take was the same as yours. However, I would be of the opinion that the program would definitely be better if it returned non-zero on failure, so the question for me is whether it rises to the level of "bug" or not. In retrospect I can't think of when I wouldn't consider a program silently failing to not be a bug (unless specifically designed to silently fail), so I've come round to agreement with the article.
Sounds like we need to use https://github.com/Hello-World-EE/Java-Hello-World-Enterpris... to cover all our bases.
stdio is program input, and a program's user should be informed about bad inputs. that said, usually, where hello world is usually demonstrated is far away from i/o so perhaps the negligence. but to argue that it's behaving correctly here is unnecessary.