Well that's precisely the mindset of C/C++. You have to think by yourself about everything that can go wrong with your code. And, man, lots of things can go wrong.
I find more modern languages so much less exhausting to use to write correct code.
I just realized I never thought about why System.out.println doesn't declare an IOException. Turns out PrintStream silently catches the exception and turns it into an error flag no one ever checks. Undermining both the Ability to handle IO errors using exceptions and making it impossible to find out what happened over what I assume was the ability to call System.out.println without checking for errors. Right now I am just happy that my IO code generally writes to binary streams so I don't have to rush through my code base to check for that nasty surprise.
I am not sure either follows. But it depends how we even define "older languages", especially considering differences between python 3 and 2, are they the same age (based on the original python release) or are they treated for their respective release version?
Just taking some simple release dates [1] or wikipedia I found:
With the ambiguity around what "age" even means for the language here (e.g., counting the age of node.js or python) it is probably meaningless, but it seems well mixed independent of age.
You could have a way to explicitly flag that, instead C will assume that any accidentally silently discarded result can be optimized around with and kick of a landslide of changes that turns bugs ten times worse.
Yes. First you learn to 'code stuff' and as your experience progresses in that language you merely learn more and more of the ways it can be wrong, and then you worry more, and have to overthink every little thing.
The hidden costs are enormous and to this day still not very well accounted for.
I find more modern languages so much less exhausting to use to write correct code.
Modern languages do catch more programmer errors than C/C++, but the more general point is that there are "edge cases" (redirecting to a file isn't an edge case) that developers need to consider that aren't magically caught, and understanding the language you use well enough so as not to write those bugs is important.
The more experience I get as a dev the more I've come to understand that building the functionality required in a feature is actually a very small part of the job. The "happy path" where things go right is often trivial to code. The complexity and effort lies in making sure things don't break when the code is used in a way I didn't anticipate. Essentially experience means anticipating more ways things can go wrong. This article is a good example of that.
> Modern languages do catch more programmer errors than C/C++, but the more general point is that there are "edge cases" (redirecting to a file isn't an edge case) that developers need to consider that aren't magically caught, and understanding the language you use well enough so as not to write those bugs is important.
But GP’s point is that modern languages can surface those issues and edge cases, and try to behave somewhat sensibly, but even sometimes “magically” report the edge cases in question.
That’s one of the things which is very enjoyable (though sometimes frustrating) in Rust, the APIs were (mostly) designed such that you must acknowledge all possible errors somehow, either handling it or explicitly suppressing it.
Indeed. One of the things you notice when writing say, Advent of Code solutions in Rust is that you're writing unwrap() a lot e.g. something like
let geese = usize::from_str_radix(line.strip_prefix("and also ").unwrap().strip_suffix(" geese.").unwrap(), 10).unwrap();
All these functions, usize::from_str_radix, str::strip_prefix, str::strip_suffix are Options which could be None, and we need to decide what to do with that option, Option::unwrap() says I promise this is Some(thing) and I want that thing. If you're wrong and the Option was None, Rust will panic.
Sure enough sometimes while rushing to solve an AoC problem you may get told your program panicked because you tried to unwrap None. Oops, I wrote "geese" above and thus didn't handle the scenario where it might say "goose" in the input when there was only one of them... need to handle that properly.
In a C program taking the same approach it's easy to skip that unwrap() step. After all we're sure it's fine... aren't we? And in the absence of any error reporting now all you know is AoC says the answer you got was wrong and you've got no idea what the problem is.
A modern language could automatically throw an exception if the string cannot be completely written to standard output.
But that has not necessarily helped. The program now has a surprising hidden behavior; it has a way of terminating with a failed status that is not immediately obvious.
If it is used in a script, that could bite someone.
In Unix, there is such an exception mechanism for disconnected pipes: the SIGPIPE error. That can be a nuisance and gets disabled in some programs.
C/C++ basically do only exactly what you tell them and nothing more, which is why they're so much faster than other languages.
There's no garbage collection/reference counting/etc. going on in the background. Objects aren't going to be moved around unless you explicitly move them around (Enjoy your heap fragmentation!). In C, you don't even get exceptions.
Of course, this creates TONS of foot-guns. Buffer overflows, unchecked errors, memory leaks, etc. A modern language won't have these, except for memory leaks, but they're much less likely to happen in trivial to moderate complexity apps.
This is true, however if we modify the program to print a 4096-byte long string instead of just the "hello world" string, then it's not sufficient again. And of course, the number 4096 is system-dependent.
So to really do hello world in C right, in addition to fflush, you also need to check the return value from puts. I've never seen any C tutorial do that though.
Because errors on FILE streams are persisted until clearerr, it should be sufficient to check the return value of fflush at the end of the program. Presumably the FILE I/O interface was deliberately designed this way, so error checking could be consolidated at the end of a series of operations.
What would be the expected reaction if either puts or fflush returns an error code? You might think, write a message on stderr (which may be different from stdout), but what if stderr is also redirected to a full device? How would you react to the error code returned from that?
To me this is an indication that you need to know the context in which the program gets run, and its purpose in that context. Or you'd have to specify every edge case, but I've never seen that really work in practice.
> What would be the expected reaction if either puts or fflush returns an error code? You might think, write a message on stderr (which may be different from stdout), but what if stderr is also redirected to a full device? How would you react to the error code returned from that?
I don't think you would react any differently on stderr failure unless you had a more complex system with alternate ways to report such things configured.
Just ignore it and continue to report the major error (stdout) in your exit status.
Even Golang de facto suffers from this. I don't think I can name a time I saw someone check the return value of fmt.Print or log.Print. Not checking the return value still seems the the "right" thing to do.
I do. But then I’m writing a shell (like Bash/Fish/etc but more DevOps focused) so if I don’t handle all types of errors then the entire UX falls apart.
Imo this is because the responsibility is not clearly defined and can be argued upon.
If my program writes to the standard output, but you choose to redirect the pipe to a different location, is it my program’s responsibility to check what happens to the bytes AFTER the pipe?
After all: my program did output everything as expected. The part which fucked up was not part of my program.
I can see why some projects decide to not handle this bug.
> is it my program’s responsibility to check what happens to the bytes AFTER the pipe?
No, but it's not "after". Rather, it's your responsibility to handle backpressure by ensuring the bytes were written to the pipe successfully in the first place.
This isn't just about the filesystem being full btw. If you imagine a command like ./foo.py | head -n 10, it only makes sense for the 'head' command to close the pipe when it's done, and foo.py should be able to detect this and stop printing any more output. (This is especially important if you consider that foo.py might produce infinite lines of output, like the 'yes' program.)
I would argue this is not necessarily even an error from a user standpoint, so the return code from food.py should still be zero in many cases—a pipe-is-closed error just means the consumer simply didn't want the rest of the output, which is fine [1], whereas an out-of-disk-space error is probably really an error. Handling these robustly is actually difficult though, because (a) you'd need to figure out why printf() failed (so that you can treat different failures differently—but it's painful), and (b) you need to make sure any side effects in the program flow up to the printf() are semantically correct "prefixes" of the overall side effect, meaning that you'd need to pay careful attention to where you printf(). (Practically speaking, this makes it difficult to even have side effects that respect this, but that's an inherent problem/limitation of the pipeline model...)
FWIW, I would be very curious if anyone has formalized all of these nuances of the pipeline model and come up with a robust & systematic way to handle them. It seems like a complicated problem to me. To give just one example of a problem that I'm thinking of: should stderr and stdout behave the same way with respect to "pipe is closed"? e.g. should the program terminate if either is closed, or if both are closed? The answer is probably "it depends", but on what exactly? What if they're redirected externally? What if they're redirected internally? Is there a pattern you can follow to get it right most of the time? There's a lot of room for analysis of the issues that can come up, especially when you throw buffering/threading/etc. into the mix...
[1] Or maybe it isn't. Maybe the output (say, some archive format like ZIP) has a footer that needs to be read first, and it would be corrupt otherwise. Or maybe that's fine anyway, because the consumer should already understand you're outputting a ZIP, and it's on them if they want partial output. As always, "it depends". But I think a premature stdout closure is usually best treated as not-an-error.
What the pipe does is orthogonal to what the programme should do. The problem here is that errors are not being handled. There are languages such as rust that enforce error handling, whereby the policy on error is made explicit. The nuances you highlight are around what the errors should describe, which ultimately leads to more potential granularity in the error policy.
> This isn't just about the filesystem being full btw. If you imagine a command like ./foo.py | head -n 10, it only makes sense for the 'head' command to close the pipe when it's done, and foo.py should be able to detect this and stop printing any more output.
The usual way of handling this is by not (explicitly) handling it. Writes to a closed pipe are special, they do not normally fail with a status that the program then all too often ignores, they result in a SIGPIPE signal that defaults to killing the process. Extra steps are needed to not kill the process. No other kind of write error gets this special treatment that I am aware of.
That's a POSIX thing. It doesn't apply to all C implementations but it does apply to many more than just Linux-based ones. You've not got a closed pipe so you wouldn't see it, you've just got a closed file descriptor. Try running it as
./a.out | :
and you will probably see it. I say probably because there is a timing aspect as well, the write may happen before the pipe gets closed in which case it will not fail, but it is unlikely to.
Yeah I should've said POSIX, my bad. But yeah my point was it's not plain C behavior.
And yes on Linux I do see it with your no-op example now. Though for some reason not with 'head'... what's going on? Is it not closing the pipe when it exits?
$ printf '%s\n' '#include <stdio.h>' '#include <unistd.h>' 'int main() { setvbuf(stdout, NULL, _IONBF, 0); int r = puts("Starting...\n"); r += fputs("First\n", stdout); fflush(stdout); usleep(1000000); fprintf(stderr, "%d\n", r); }' | cc -x c - && ./a.out | head -n 1
Starting...
19
We know that b and c both happen after a, and that d happens after c. However, we do not know whether b happens before c, between c and d, or after d. Your a.out process will only get killed by SIGPIPE if it happens after d.
On my system, running a.out under strace is enough to slow it down enough to affect the timing and see the SIGPIPE you were expecting. You may alternatively insert artificial delays in your test program such as by calling the sleep() function between the two lines of output to see the same result.
Sorry, I think I edited my comment while you were replying. But I just noticed the problem in the most recent version was that I didn't write to stdout after the usleep(), so it never raised SIGPIPE. Thanks.
I don’t personally agree with that judgement. While the failure condition is at the OS level it’s still affecting the function of the program an in unexpected way.
Plus the whole point of STDOUT is that it is a file. So it shouldn’t change the developers mental model if that file happens to be a pseudo TTY, a pipe or a traditional persistent file system object. This flexibility is one of the core principles of UNIX and it’s what makes the POSIX command line as flexible as it is.
With that in mind, is this criticism of the Java hello world valid? Its output abstracts more than stdout and maybe in Windows this but would not occur. (I don't know, just discussing)
I thought about this too a bit just now. But I think it's not the shell setting up stuff outside your process that then fails. Rather you already get handles to the "full" file system at process creation and then it's your problem. And traditionally, the behaviour you get from all the standard streams is very unpredictable, depending on where they point.
In this scenario you didn’t write any bytes though. You made a call to write to standard out (your process’s file handle 1) and didn’t succeed, you didn’t handle the possible error condition, you just silently ignored it.
I think this is pretty cut and dried - the failure is inside your process’s address space and the programmer error is that you haven’t handled a reported error.
>> what happens to the bytes AFTER the pipe?
There isn’t a pipe involved here, when your process was created it’s stdout was connected to dev/full then your program began executing
> you didn’t handle the possible error condition, you just silently ignored it.
Problem is, the error condition is not even that obvious. I tried it, and printf() will happily return the number of bytes written, even when redirecting stdout to /dev/full.
I am not 100% sure, but I think this has to do with the fact that printf uses buffered io, and writing the bytes to the buffer will work. It's only when the buffer is flushed that this will become a problem, but this would need to be handled in the code to show an error message.
Your program has a bug because it can write nothing or just part and will always return zero exit code. Ie think about using your program as part of bash script where you often rely on process exit codes.
> but you choose to redirect the pipe to a different location
The output doesn't go into a pipe however, the output goes to /dev/full. Redirection happens before the process is started, so the program is, in fact, writing directly to a file descriptor that returns an error upon write.
I feel like this is misrepresenting the article's point which isn't literally "hello world is buggy if it returns success on failure" but more "you should do error-handling". In this very specific case, you can argue that it's irrelevant. But if your program writes to a log file, or writes to a data file that it later reads from, it had better include some error-handling.
The fact that there's redirection is a ... misdirection. The redirection is only used to proxy a real-life case that can happen even when no redirection is taking place.
What's the real-life case where hello world fails because the file system is full?
You could do all kinds of things that would cause hello world to "fail". A broken monitor (or even one unplugged) wouldn't show "hello world" or give any indication of an error too, but it's hardly the codes fault. The code does what it's supposed to and ignores all kinds of other things that could go horribly wrong. That's not really a bug, just a known and expected limitation of the program's scope.
As I said, not hello world necessarily, but any program that writes output can encounter this problem, and there's an overlap with general file-writing even.
> If my program writes to the standard output, but you choose to redirect the pipe to a different location, is it my program’s responsibility to check what happens to the bytes AFTER the pipe?
The pipe is your standard output. Your very program is created with the pipe as its stdout.
> After all: my program did output everything as expected. The part which fucked up was not part of my program.
But you are wrong, your program did not output everything as expected, and it failed to report that information.
It's a fun take, but a hyperbole nonetheless. hello.c is supposed to be run from a terminal and write back to it: there's always space to write. It's not meant to be part of a shell script, so the error status is irrelevant.
It does show that we take such examples a bit too literally: our feeble minds don't consider what's missing, until it's too late. That's a didactic problem. It only matters to certain kinds of software, and when we teach many people to program, most of them won't go beyond a few small programs. But perhaps the "second programming course" should focus a bit less on OOP and introduce error handling.
It depends on whether you want your Hello World programs to reflect an actual program or just be an approximation.
I’d argue there is little benefit in the latter. Particularly these days where the Hello World of most imperative languages look vaguely similar. Maybe back when LISP, FORTRAN and ALGOL were common it was more useful showing a representation of the kind of syntax one should expect. But that isn’t the case any more.
Plus given the risk of bugs becoming production issues or, worse, security vulnerabilities and the ease and prevalence of which developers now copy and paste code, I think there is now a greater responsibility for examples to make fewer assumptions. Even if that example is just Hello World.
> It depends on whether you want your Hello World programs to reflect an actual program or just be an approximation. I’d argue there is little benefit in the latter.
There's a huge benefit in having a program that verifies you have set up the programming environment successfully and can build and execute your programs. Far more than the didactic benefit of any "Hello World" program.
Handling terminal output is just an extra nice-to-have at that point, and one convenient way to verify your tools are working. Correct error handling is definitely out of scope.
Interesting take but I see two problems with that:
1. if you're testing your development environment then handling errors appropriately is even more important. The last thing you want to find out is that your development environment doesn't work because of some edge case that wasn't tested.
2. if your code is just to test the development environment then ship that test code with the development environment rather than publish it on your home page as a practical example of your languages code.
What you're describing is effectively a behavioral test, not a Hello World example.
If you don't know how the program is going to be used, how do you know that it is doing the right thing? Is "Hello world" actually the expected output?
Also, what makes the status code handling special compared to, say,
- assuming the english language is the preferred language instead of asking the OS for the user's preference
- assuming that the output has to be ASCII (or compatible) instead of something like UTF-16
There seems to be a weird obsession with the program's status code over anything else in this whole comment section, and it seems to me that the only reason for that is that back in the stone age of computing, the status code was the only thing that got standardized, while locale and encoding didn't, so properly supporting the latter is hard and therefore assumed to be less important.
> Also, what makes the status code handling special compared to, say, - assuming the english language is the preferred language instead of asking the OS for the user's preference - assuming that the output has to be ASCII (or compatible) instead of something like UTF-16
True. But by any definition printing nothing is a failure. If failing to print anything isn't a failure, what is?
> There seems to be a weird obsession with the program's status code over anything else in this whole comment section
It's because it's the only structured way to indicate success. And "not printing" is clearly a failure of "print 'hello world'".
> supporting the latter is hard and therefore assumed to be less important.
No, but printing nothing is clearly a failure. Printing the wrong language is not obviously a failure of helloworld, and absolutely not something helloworld can know on its own.
helloworld can know it failed to print what it intended to print. It cannot confirm that its intentions are correct, even in principle.
It cannot know whether some Lennartware has decided that anything built before yesterday (e.g. LANG envs and friends) can be ignored, and that in Lennart land all programs should write a request for the language to the blockchain, and wait for a reply transaction before printing anything.
Just because you can come up with examples of errors helloworld cannot check, doesn't mean that it should not do error handling for the things that it can.
`tar` cannot check that the user actually intended the particular file format flavor that it implements, but it can know that failing to write it should cause it to report an error.
The first piece of C code in introduction section was meant as production software? I've checked it: that section mentions typing "a.out" in the the UNIX shell to see what happens.
It's not necessarily an error to print less than you intended though. The consumer might have simply decided that they didn't need the rest of the input. Whether or not it's an error depends on why the write failed to occur. Usually out-of-space is an error, whereas pipe-is-closed/has-reached-EOF is not.
This raises an interesting question: is there any IO function that should return unit/void? Or equivalently are there any IO functions for which we can safely ignore the return value/ignore all exceptions?
It seems like every single IO thing I can think of can have a relevant error, regardless of whether it's file-system related, network, or anything else.
I think you can certainly return void, and you can ignore any I/O exceptions up to the top layer of the stack, but then you have to decide whether the exception should result in an error code to the user or not. Some (like "out of disk space") are usually errors, whereas others (like "no more data" or "pipe is closed") may not be.
In C, and many other languages, the file stream error state is saved after each operation, so you can skip error checking on every output line and only do
I wish this post were higher up, since it shows the idiomatic way to deal with that problem, unlike the article. Obviously the designers of the Unix i/o interface thought about this and provided for a simple way of handling it.
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.
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).
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
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.
> [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.
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.
> 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.
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.
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.
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.
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.
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.
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.
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).
> 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).
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.
> There's our "No space" error getting reported by the OS, but no matter, the program silently swallows it and returns 0, the code for success. That's a bug!
Bzzt, no. You can't say that without knowing what the program's requirements are.
Blindly "fixing" a program to indicate failure due to not being able to write to standard output could break something.
Maybe the output is just a diagnostic that's not important, but some other program will reacts to the failed status, causing an issue.
Also, if a program produces output with a well-defined syntax, then the termination status may be superfluous; the truncation of the output can be detected by virtue of that syntax being incomplete.
E.g. JSON hello world fragment:
puts("{\"hello\":\"world\"}");
return 0;
if something is picking up the output and parsing it as JSON, it can deduce from a failed parse that the program didn't complete, rather than going by termination status.
> Also, if a program produces output with a well-defined syntax, then the termination status may be superfluous; the truncation of the output can be detected by virtue of that syntax being incomplete.
The author covers this (or rather, the possibility that truncation can not be detected).
> if something is picking up the output and parsing it as JSON, it can deduce from a failed parse that the program didn't complete, rather than going by termination status.
This is bad advice. Consider output that might be truncated but can't be detected (mentioned in the article).
The exit status is the only reliable way to detect failures (unless you have a separate communication channel and send a final success message).
My remark "if a program produces output with a well-defined syntax" was intended specifically to consider such cases, and set them aside.
I didn't communicate that clearly: syntax can be "well-defined" yet truncatable. I meant some kind of syntax that is invalid if any suffix is missing, including the entire message, or else an object of an unexpected type is produced.
(In the case of JSON, valid JSON could be output which is truncatable, like 3.14 versus 3.14159. If the output is documented and expected to be a dictionary, we declare failure if a number emerges.)
There is more nuance to this, which is that we cannot detect all modes of failure just because we have written to a file object, and successfully flushed and closed it.
In the case of file I/O, we do not know that the bits have actually gone to the storage device. A military-grade hello world has to perform a fsync. I think that also requires the right storage hardware to be entirely reliable.
If stdout happens to be a TCP socket, then all we know from a successful flush and close is that the data has gone into the network stack, not that the other side has received it. We need an end-to-end application level ack. (Even just a two-way orderly shutdown: after writing hello, half-close the socket. Then read from it until EOF. If the read fails, the connection was broken and it cannot be assumed that the hello had been received.)
This issue is just a facet of a more general problem: if the goal of the hello world program is to communicate its message to some destination, the only way to be sure is to obtain an acknowledgement from that destination: communication must be validated end-to-end, in other words. If you rely on any success signal of an intermediate agent, you don't have end-to-end validation of success.
The super-robust requirements for hello world therefore call for a protocol: something like this:
Now we can detect failures like that there is no user present at the console who is reading the message. Or that their monitor isn't working so the can't read the question.
We can now correctly detect this case of not being able to deliver hello, world, converting it to a failed status:
$ ./hello < /dev/null > /dev/null
We can still be lied to, but there is strong justification in regarding that as not our problem:
$ yes | ./hello > /dev/null
We cannot get away from requiring syntax, because the presence of a protocol gives rise to it; the destination has to be able to tell somehow when it has received all of the data, so it can acknowledge it.
A super reliable hello world also must not take data integrity for granted; the message should include some kind of checksum to reduce the likelihood of corrupt communication going undetected.
In the end, it states that the language C has the bug. But this is wrong. In C, there are no exceptions, i.e. all error checking has to be explicit. This is just the language. So when you now ignore the error, this is not a bug of the language but just a bug in your code. The only thing you could argue is that this is a bad language design.
Or maybe this about global stdout object. With buffering enabled (by default), printf will not throw any error. The fflush would do. But a final fflush would be done implicitly at the end. But this is again all well documented, so still, this is not really a bug but maybe just bad language design.
I'm not exactly sure what C++ code was used. If this was just the same C code, then the same thing applies. And iostream just behaves exactly as documented.
puts(), like printf() and all the C-standardised "stdio" functions use buffered writes. So that is also buggy, because the buffer won't be flushed until after main() returns. You need to call and check the return value of "fflush(stdout)" manually to get the correct result.
puts() returns 13 with no pipe and with pipe to /dev/full, which I just learned is due to buffering.
What worked for me initially was the POSIX write() function:
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
int status;
status = write(1, "Hello World!\n", 13);
if (status < 0) { return EXIT_FAILURE; }
return EXIT_SUCCESS;
}
-----
As someone else commented, fflush() gives the desired error response.
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int status;
puts("Hello World!");
status = fflush(stdout);
if (status < 0) { return EXIT_FAILURE; }
return EXIT_SUCCESS;
}
-----
andreyv probably has the best alternative[1], which is checking fflush() and ferror() at the program's end and calling perror(). It's better because it outputs an actual error message on the current terminal, and you don't need to write a special error checking wrapper.
2. Node.js is not a language. JavaScript is a language, and Node.js is a JavaScript runtime environment that runs on the V8 engine and executes JavaScript code outside a web browser.
Since macOS does not have /dev/full, I think what is actually happening here is your bash shell fails to create a file named "full" in "/dev" and so the bash shell exits with an error; this has nothing to do with node.js.
Now I'm curious about another interesting question. Should bash be the one that handle the error and exit code in this case? Since it seems to be responsible for handing the piping operation.
If by 'in this case' you mean the problem mentioned by the linked article, then no. The shell is not 'responsible for handling the piping operation'. It creates the pipe, but is not responsible for moving data through it.
This is because console.log isn't the equivalent to the post's printf. It is purposefully opaque to the application (and applications should not assume anything happens with the input).[0]
> Its main output is the implementation-defined side effect of printing the result to the console.
> 2. Node.js is not a language. JavaScript is a language,
This criticism is the wrong way around. All of the author's "languages" are actually language implementations like NodeJS. You can tell because he produced the results by running the code, rather than by reading a spec.
So what I'm proposing is to put JavaScript in the language column (like other languages such as Java) and note the usage of Node.js as the implementation in the second column together with version (similar to Java ->
openjdk 11.0.11 2021-04-20).
That would certainly make sense, but the author hasn't followed that pattern consistently so far - e.g. the row for C doesn't list the compiler or architecture.
Definitely thought-provoking. A few responses here on HN disagree with calling this a bug, so maybe the user owns the error. This is all related to what kind of contract we have in mind when creating and using such a program.
If `puts` were to be used for debug messages, it might be right not to fail so as to not disturb the rest of the program. If the primary purpose is to greet the world, then we might expect it to signal the failure. But each creator or user might have their own expected behaviors.
If a user expects different behavior, then perhaps it is a feature request:
The question is how the behavior can be made more explicit. I think it's a reasonable default to make programs fail often and early. If some failure can be safely ignored, it can always be implemented as an (explicit) feature.
I think it's clearly main() that "owns" that error, since it's the one that swallowed it. It would be impossible for the shell to own it since it's impossible for the shell to even detect it, given this program's buggy behavior.
I find the argument that the code obviously ignores the error so that's obviously the program's intent to be completely spurious. The code "obviously" intends to print the string, too, and yet in some cases, it doesn't actually do that. It's clearly a bug. I don't think it's particularly useful to harp on this bug in the most introductory program ever, but it's definitely a bug.
You all joke that this doesn’t happen in practice, but something like this literally just bit me and it took me a few too many minutes to figure out what was going on.
I use a bash script as my BROWSER which calls another bash script to launch or communicate with my browser that I run inside a container. The script that my BROWSER script calls has some debug output that it prints to stderr.
I use mutt as my email client and urlscan [0] to open URLs inside emails. Urlscan looks at my BROWSER environment variable and thus calls my script to open whatever URL I target. Some time recently, the urlscan author decided to improve the UX by hiding stderr so that it wouldn’t pollute the view, and so attempted to pipe it to `/dev/null`. I guess their original code to do this wasn’t quite correct and it ended up closing the child processes’ stderr.*
I generally use `set -e` (errexit) because I want my scripts to fail if any command fails (I consider that after an unhandled failure the script’s behavior is undefined, some other people disagree and say you should never use `set -e` outside of development, but I digress). My BROWSER scripts are no exception.
While my scripts handle non-zero returns for most things that can go wrong, I never considered that writing log messages to stdout or stderr might fail. But it did, which caused the script to die before it was able to launch my browser. For a few weeks I wasn’t able to use urlscan to open links. I was too lazy to figure out what was wrong, and when I did it took me a while because I looked into every possibility except this one.
Luckily this wasn’t a production app. But I know now it could just as feasibly happen in production, too.
I opened an issue[1] and it was fixed very quickly. I love open source!
*No disrespect to urlscan, it’s an awesome tool and bugs happen to all of us!
It sounds our sensibilities are similar regarding cli and tool usage. This is a side note, but as someone who used to use "Bash strict mode" in all my scripts, I'm now a bit bearish on `set -e`, mainly due to the subtle caveats. If you're interested, the link below has a nice (and long) list of potentially surprising errexit gotchas:
I think the idea is you use set -e during development to find where you should catch errors, but in production you may want it off to reduce strange side-effects (or explicitly check for success in the way you expect; so not that the command returned 0 but that the file it made exists and is the right length, etc).
They operate the way I expect. set -e fails when I don't handle the error code of a command. If my script eat it, it doesn't fail. I see no problems here.
That seems like a really weak argument. Sometimes set -e won't catch an error, therefore it's better to let all errors slip through? "You're supposed to handle every error." Yeah, okay, set -e doesn't interfere with that.
> but in production you may want it off to reduce strange side-effects
Having -e set is to reduce strange side-effects, by having the script fail, instead of plowing headlong into the land of undefined/unexpected behavior.
The `if` bit should be well-known if you're writing bash. The pipe bit is unfortunate, and is why -o pipefail is recommended, too. Or, just writing in a sane language that isn't going to page you in the middle of the night.
I run Firefox inside a systemd-nspawn[0] container. I wrote a little wrapper around systemd-nspawn that I call arch-lwc[1] which kinda mimics the docker CLI. I have another script to coordinate the Firefox-specific stuff.
Thanks for sharing!
I wish this functionality was better exposed, it is such a game changer. I need to clean up and publish some of my scripts. I like to use firejail, xpra, and I'm trying to improve btrfs ephemeral subvolumes for my sandboxes.
261 comments
[ 3.7 ms ] story [ 337 ms ] threadI find more modern languages so much less exhausting to use to write correct code.
Just taking some simple release dates [1] or wikipedia I found:
Ages of "Yes" group: 49, 36, 22, 26, 26, 12, 31
Ages of "No" group: 11, 14, 33, 7, 32, 45, 26, 34, 21
Averages: Yes 28.85, No 24.78
With the ambiguity around what "age" even means for the language here (e.g., counting the age of node.js or python) it is probably meaningless, but it seems well mixed independent of age.
[1] https://blog.sunfishcode.online/bugs-in-hello-world/
[1] https://iq.opengenus.org/age-of-programming-languages/
The hidden costs are enormous and to this day still not very well accounted for.
Modern languages do catch more programmer errors than C/C++, but the more general point is that there are "edge cases" (redirecting to a file isn't an edge case) that developers need to consider that aren't magically caught, and understanding the language you use well enough so as not to write those bugs is important.
The more experience I get as a dev the more I've come to understand that building the functionality required in a feature is actually a very small part of the job. The "happy path" where things go right is often trivial to code. The complexity and effort lies in making sure things don't break when the code is used in a way I didn't anticipate. Essentially experience means anticipating more ways things can go wrong. This article is a good example of that.
But GP’s point is that modern languages can surface those issues and edge cases, and try to behave somewhat sensibly, but even sometimes “magically” report the edge cases in question.
That’s one of the things which is very enjoyable (though sometimes frustrating) in Rust, the APIs were (mostly) designed such that you must acknowledge all possible errors somehow, either handling it or explicitly suppressing it.
let geese = usize::from_str_radix(line.strip_prefix("and also ").unwrap().strip_suffix(" geese.").unwrap(), 10).unwrap();
All these functions, usize::from_str_radix, str::strip_prefix, str::strip_suffix are Options which could be None, and we need to decide what to do with that option, Option::unwrap() says I promise this is Some(thing) and I want that thing. If you're wrong and the Option was None, Rust will panic.
Sure enough sometimes while rushing to solve an AoC problem you may get told your program panicked because you tried to unwrap None. Oops, I wrote "geese" above and thus didn't handle the scenario where it might say "goose" in the input when there was only one of them... need to handle that properly.
In a C program taking the same approach it's easy to skip that unwrap() step. After all we're sure it's fine... aren't we? And in the absence of any error reporting now all you know is AoC says the answer you got was wrong and you've got no idea what the problem is.
>In a C program taking the same approach (...)
"Writing C" and "rushing" is a strategy that cannot give you correct, robust software.
A modern language could automatically throw an exception if the string cannot be completely written to standard output.
But that has not necessarily helped. The program now has a surprising hidden behavior; it has a way of terminating with a failed status that is not immediately obvious.
If it is used in a script, that could bite someone.
In Unix, there is such an exception mechanism for disconnected pipes: the SIGPIPE error. That can be a nuisance and gets disabled in some programs.
There's no garbage collection/reference counting/etc. going on in the background. Objects aren't going to be moved around unless you explicitly move them around (Enjoy your heap fragmentation!). In C, you don't even get exceptions.
Of course, this creates TONS of foot-guns. Buffer overflows, unchecked errors, memory leaks, etc. A modern language won't have these, except for memory leaks, but they're much less likely to happen in trivial to moderate complexity apps.
So to really do hello world in C right, in addition to fflush, you also need to check the return value from puts. I've never seen any C tutorial do that though.
To me this is an indication that you need to know the context in which the program gets run, and its purpose in that context. Or you'd have to specify every edge case, but I've never seen that really work in practice.
I don't think you would react any differently on stderr failure unless you had a more complex system with alternate ways to report such things configured.
Just ignore it and continue to report the major error (stdout) in your exit status.
int main(void) { return !(puts("Hello, new world!") == EOF); /* return 0 unless a rare I/O error occurs */ }
Most Go in the wild is doing way more than a typical *nix binary, so the use case differs.
If you want a resilient system, you don't die on print and log failures.
https://gobyexample.com/hello-world
Checking the result of log and print is very tedious and not useful most of the time.
I'm going to have to go back over all the print statements I've ever written now
Yes it is. And it specifies the OS as well.
https://gist.github.com/koral--/12a6cdda22ffbd82f28ecc93e0b5...
If my program writes to the standard output, but you choose to redirect the pipe to a different location, is it my program’s responsibility to check what happens to the bytes AFTER the pipe?
After all: my program did output everything as expected. The part which fucked up was not part of my program.
I can see why some projects decide to not handle this bug.
No, but it's not "after". Rather, it's your responsibility to handle backpressure by ensuring the bytes were written to the pipe successfully in the first place.
This isn't just about the filesystem being full btw. If you imagine a command like ./foo.py | head -n 10, it only makes sense for the 'head' command to close the pipe when it's done, and foo.py should be able to detect this and stop printing any more output. (This is especially important if you consider that foo.py might produce infinite lines of output, like the 'yes' program.)
I would argue this is not necessarily even an error from a user standpoint, so the return code from food.py should still be zero in many cases—a pipe-is-closed error just means the consumer simply didn't want the rest of the output, which is fine [1], whereas an out-of-disk-space error is probably really an error. Handling these robustly is actually difficult though, because (a) you'd need to figure out why printf() failed (so that you can treat different failures differently—but it's painful), and (b) you need to make sure any side effects in the program flow up to the printf() are semantically correct "prefixes" of the overall side effect, meaning that you'd need to pay careful attention to where you printf(). (Practically speaking, this makes it difficult to even have side effects that respect this, but that's an inherent problem/limitation of the pipeline model...)
FWIW, I would be very curious if anyone has formalized all of these nuances of the pipeline model and come up with a robust & systematic way to handle them. It seems like a complicated problem to me. To give just one example of a problem that I'm thinking of: should stderr and stdout behave the same way with respect to "pipe is closed"? e.g. should the program terminate if either is closed, or if both are closed? The answer is probably "it depends", but on what exactly? What if they're redirected externally? What if they're redirected internally? Is there a pattern you can follow to get it right most of the time? There's a lot of room for analysis of the issues that can come up, especially when you throw buffering/threading/etc. into the mix...
[1] Or maybe it isn't. Maybe the output (say, some archive format like ZIP) has a footer that needs to be read first, and it would be corrupt otherwise. Or maybe that's fine anyway, because the consumer should already understand you're outputting a ZIP, and it's on them if they want partial output. As always, "it depends". But I think a premature stdout closure is usually best treated as not-an-error.
The usual way of handling this is by not (explicitly) handling it. Writes to a closed pipe are special, they do not normally fail with a status that the program then all too often ignores, they result in a SIGPIPE signal that defaults to killing the process. Extra steps are needed to not kill the process. No other kind of write error gets this special treatment that I am aware of.
That said though, I can't even reproduce what you're saying on Linux:
And yes on Linux I do see it with your no-op example now. Though for some reason not with 'head'... what's going on? Is it not closing the pipe when it exits?
Edit: D'oh, see below.a) a.out writes line 1
b) a.out writes line 2
c) head reads line 1
d) head closes the pipe
We know that b and c both happen after a, and that d happens after c. However, we do not know whether b happens before c, between c and d, or after d. Your a.out process will only get killed by SIGPIPE if it happens after d.
On my system, running a.out under strace is enough to slow it down enough to affect the timing and see the SIGPIPE you were expecting. You may alternatively insert artificial delays in your test program such as by calling the sleep() function between the two lines of output to see the same result.
But pipes aren't a C thing in the first place. "unistd.h" is not a C thing, file descriptors aren't a C thing.
And not every platform with pipes supports SIGPIPE with such behavior.
Plus the whole point of STDOUT is that it is a file. So it shouldn’t change the developers mental model if that file happens to be a pseudo TTY, a pipe or a traditional persistent file system object. This flexibility is one of the core principles of UNIX and it’s what makes the POSIX command line as flexible as it is.
I think this is pretty cut and dried - the failure is inside your process’s address space and the programmer error is that you haven’t handled a reported error.
>> what happens to the bytes AFTER the pipe?
There isn’t a pipe involved here, when your process was created it’s stdout was connected to dev/full then your program began executing
Problem is, the error condition is not even that obvious. I tried it, and printf() will happily return the number of bytes written, even when redirecting stdout to /dev/full.
I am not 100% sure, but I think this has to do with the fact that printf uses buffered io, and writing the bytes to the buffer will work. It's only when the buffer is flushed that this will become a problem, but this would need to be handled in the code to show an error message.
The output doesn't go into a pipe however, the output goes to /dev/full. Redirection happens before the process is started, so the program is, in fact, writing directly to a file descriptor that returns an error upon write.
The fact that there's redirection is a ... misdirection. The redirection is only used to proxy a real-life case that can happen even when no redirection is taking place.
You could do all kinds of things that would cause hello world to "fail". A broken monitor (or even one unplugged) wouldn't show "hello world" or give any indication of an error too, but it's hardly the codes fault. The code does what it's supposed to and ignores all kinds of other things that could go horribly wrong. That's not really a bug, just a known and expected limitation of the program's scope.
The pipe is your standard output. Your very program is created with the pipe as its stdout.
> After all: my program did output everything as expected. The part which fucked up was not part of my program.
But you are wrong, your program did not output everything as expected, and it failed to report that information.
[0] https://zig.news/kristoff/where-is-print-in-zig-57e9
It does show that we take such examples a bit too literally: our feeble minds don't consider what's missing, until it's too late. That's a didactic problem. It only matters to certain kinds of software, and when we teach many people to program, most of them won't go beyond a few small programs. But perhaps the "second programming course" should focus a bit less on OOP and introduce error handling.
I’d argue there is little benefit in the latter. Particularly these days where the Hello World of most imperative languages look vaguely similar. Maybe back when LISP, FORTRAN and ALGOL were common it was more useful showing a representation of the kind of syntax one should expect. But that isn’t the case any more.
Plus given the risk of bugs becoming production issues or, worse, security vulnerabilities and the ease and prevalence of which developers now copy and paste code, I think there is now a greater responsibility for examples to make fewer assumptions. Even if that example is just Hello World.
There's a huge benefit in having a program that verifies you have set up the programming environment successfully and can build and execute your programs. Far more than the didactic benefit of any "Hello World" program.
Handling terminal output is just an extra nice-to-have at that point, and one convenient way to verify your tools are working. Correct error handling is definitely out of scope.
1. if you're testing your development environment then handling errors appropriately is even more important. The last thing you want to find out is that your development environment doesn't work because of some edge case that wasn't tested.
2. if your code is just to test the development environment then ship that test code with the development environment rather than publish it on your home page as a practical example of your languages code.
What you're describing is effectively a behavioral test, not a Hello World example.
It's not, though.
This helloworld is not safe to use as part of something bigger. Like:
That will upload a partial file to prod, if there's any write error.> It's not meant to be part of a shell script
You don't know that. And brittle pieces like this is absolutely not an uncommon source of bugs.
Also, what makes the status code handling special compared to, say, - assuming the english language is the preferred language instead of asking the OS for the user's preference - assuming that the output has to be ASCII (or compatible) instead of something like UTF-16
There seems to be a weird obsession with the program's status code over anything else in this whole comment section, and it seems to me that the only reason for that is that back in the stone age of computing, the status code was the only thing that got standardized, while locale and encoding didn't, so properly supporting the latter is hard and therefore assumed to be less important.
Isn't that the definition of helloworld?
> Also, what makes the status code handling special compared to, say, - assuming the english language is the preferred language instead of asking the OS for the user's preference - assuming that the output has to be ASCII (or compatible) instead of something like UTF-16
True. But by any definition printing nothing is a failure. If failing to print anything isn't a failure, what is?
> There seems to be a weird obsession with the program's status code over anything else in this whole comment section
It's because it's the only structured way to indicate success. And "not printing" is clearly a failure of "print 'hello world'".
> supporting the latter is hard and therefore assumed to be less important.
No, but printing nothing is clearly a failure. Printing the wrong language is not obviously a failure of helloworld, and absolutely not something helloworld can know on its own.
helloworld can know it failed to print what it intended to print. It cannot confirm that its intentions are correct, even in principle.
It cannot know whether some Lennartware has decided that anything built before yesterday (e.g. LANG envs and friends) can be ignored, and that in Lennart land all programs should write a request for the language to the blockchain, and wait for a reply transaction before printing anything.
Just because you can come up with examples of errors helloworld cannot check, doesn't mean that it should not do error handling for the things that it can.
`tar` cannot check that the user actually intended the particular file format flavor that it implements, but it can know that failing to write it should cause it to report an error.
The first piece of C code in introduction section was meant as production software? I've checked it: that section mentions typing "a.out" in the the UNIX shell to see what happens.
It seems like every single IO thing I can think of can have a relevant error, regardless of whether it's file-system related, network, or anything else.
In GNU programs you can use atexit(close_stdout) to do this automatically.
“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-...
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
Edit : bugs are easy
The computer will do what you ask it to do, it's only a bug, when it doesn't meet your expectations.
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.
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.
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.
(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.
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.
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:
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.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/ )
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).
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.
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.
Bzzt, no. You can't say that without knowing what the program's requirements are.
Blindly "fixing" a program to indicate failure due to not being able to write to standard output could break something.
Maybe the output is just a diagnostic that's not important, but some other program will reacts to the failed status, causing an issue.
Also, if a program produces output with a well-defined syntax, then the termination status may be superfluous; the truncation of the output can be detected by virtue of that syntax being incomplete.
E.g. JSON hello world fragment:
if something is picking up the output and parsing it as JSON, it can deduce from a failed parse that the program didn't complete, rather than going by termination status.The "program's requirements" can in theory be "to be buggy unusable piece of shit". But when we speak, we don't need to consider that use case.
The author covers this (or rather, the possibility that truncation can not be detected).
This is bad advice. Consider output that might be truncated but can't be detected (mentioned in the article).
The exit status is the only reliable way to detect failures (unless you have a separate communication channel and send a final success message).
I didn't communicate that clearly: syntax can be "well-defined" yet truncatable. I meant some kind of syntax that is invalid if any suffix is missing, including the entire message, or else an object of an unexpected type is produced.
(In the case of JSON, valid JSON could be output which is truncatable, like 3.14 versus 3.14159. If the output is documented and expected to be a dictionary, we declare failure if a number emerges.)
It should always behave the same. The exit code of a program is the agreed upon standard for this.
In the case of file I/O, we do not know that the bits have actually gone to the storage device. A military-grade hello world has to perform a fsync. I think that also requires the right storage hardware to be entirely reliable.
If stdout happens to be a TCP socket, then all we know from a successful flush and close is that the data has gone into the network stack, not that the other side has received it. We need an end-to-end application level ack. (Even just a two-way orderly shutdown: after writing hello, half-close the socket. Then read from it until EOF. If the read fails, the connection was broken and it cannot be assumed that the hello had been received.)
This issue is just a facet of a more general problem: if the goal of the hello world program is to communicate its message to some destination, the only way to be sure is to obtain an acknowledgement from that destination: communication must be validated end-to-end, in other words. If you rely on any success signal of an intermediate agent, you don't have end-to-end validation of success.
The super-robust requirements for hello world therefore call for a protocol: something like this:
Now we can detect failures like that there is no user present at the console who is reading the message. Or that their monitor isn't working so the can't read the question.We can now correctly detect this case of not being able to deliver hello, world, converting it to a failed status:
We can still be lied to, but there is strong justification in regarding that as not our problem: We cannot get away from requiring syntax, because the presence of a protocol gives rise to it; the destination has to be able to tell somehow when it has received all of the data, so it can acknowledge it.A super reliable hello world also must not take data integrity for granted; the message should include some kind of checksum to reduce the likelihood of corrupt communication going undetected.
Unit testing verifies it does what it is supposed to do ideally, and all other tests verify it can do it in non-ideal environments.
edit: https://cigix.me/c17#7.21.7.9.p3
Or maybe this about global stdout object. With buffering enabled (by default), printf will not throw any error. The fflush would do. But a final fflush would be done implicitly at the end. But this is again all well documented, so still, this is not really a bug but maybe just bad language design.
I'm not exactly sure what C++ code was used. If this was just the same C code, then the same thing applies. And iostream just behaves exactly as documented.
int main(void) { if(puts("Hello, World!")!=EOF) { return EXIT_SUCCESS; }else { return EXIT_FAILURE; } }
On my test system (Ubuntu 21.10 on x86_64) the puts() call never fails.
I switched to a raw write() and that successfully catches it, by returning -1 when output is redirected to /dev/full.
Quite interesting, actually.
(And silently returning non-zero would be bad anyway.)
What worked for me initially was the POSIX write() function:
-----As someone else commented, fflush() gives the desired error response.
-----andreyv probably has the best alternative[1], which is checking fflush() and ferror() at the program's end and calling perror(). It's better because it outputs an actual error message on the current terminal, and you don't need to write a special error checking wrapper.
[1] https://news.ycombinator.com/item?id=30611924
1. Node.js result is out-dated. I run on Node.js v14.15.1 hello world code below on macOS and it reported exit code 1 correctly:
2. Node.js is not a language. JavaScript is a language, and Node.js is a JavaScript runtime environment that runs on the V8 engine and executes JavaScript code outside a web browser.3. Missing JavaScript result in the table, which is the most popular language on GitHub: https://octoverse.github.com/#top-languages-over-the-years
Now I'm curious about another interesting question. Should bash be the one that handle the error and exit code in this case? Since it seems to be responsible for handing the piping operation.
> Its main output is the implementation-defined side effect of printing the result to the console.
[0]: https://console.spec.whatwg.org/#logger
This criticism is the wrong way around. All of the author's "languages" are actually language implementations like NodeJS. You can tell because he produced the results by running the code, rather than by reading a spec.
So what I'm proposing is to put JavaScript in the language column (like other languages such as Java) and note the usage of Node.js as the implementation in the second column together with version (similar to Java -> openjdk 11.0.11 2021-04-20).
Would that make sense?
If `puts` were to be used for debug messages, it might be right not to fail so as to not disturb the rest of the program. If the primary purpose is to greet the world, then we might expect it to signal the failure. But each creator or user might have their own expected behaviors.
If a user expects different behavior, then perhaps it is a feature request:
> There's no difference between a bug and a feature request from the user's perspective. (https://blog.codinghorror.com/thats-not-a-bug-its-a-feature-...)
The question is how the behavior can be made more explicit. I think it's a reasonable default to make programs fail often and early. If some failure can be safely ignored, it can always be implemented as an (explicit) feature.
I find the argument that the code obviously ignores the error so that's obviously the program's intent to be completely spurious. The code "obviously" intends to print the string, too, and yet in some cases, it doesn't actually do that. It's clearly a bug. I don't think it's particularly useful to harp on this bug in the most introductory program ever, but it's definitely a bug.
https://news.ycombinator.com/item?id=27504254
I use a bash script as my BROWSER which calls another bash script to launch or communicate with my browser that I run inside a container. The script that my BROWSER script calls has some debug output that it prints to stderr.
I use mutt as my email client and urlscan [0] to open URLs inside emails. Urlscan looks at my BROWSER environment variable and thus calls my script to open whatever URL I target. Some time recently, the urlscan author decided to improve the UX by hiding stderr so that it wouldn’t pollute the view, and so attempted to pipe it to `/dev/null`. I guess their original code to do this wasn’t quite correct and it ended up closing the child processes’ stderr.*
I generally use `set -e` (errexit) because I want my scripts to fail if any command fails (I consider that after an unhandled failure the script’s behavior is undefined, some other people disagree and say you should never use `set -e` outside of development, but I digress). My BROWSER scripts are no exception.
While my scripts handle non-zero returns for most things that can go wrong, I never considered that writing log messages to stdout or stderr might fail. But it did, which caused the script to die before it was able to launch my browser. For a few weeks I wasn’t able to use urlscan to open links. I was too lazy to figure out what was wrong, and when I did it took me a while because I looked into every possibility except this one.
Luckily this wasn’t a production app. But I know now it could just as feasibly happen in production, too.
I opened an issue[1] and it was fixed very quickly. I love open source!
*No disrespect to urlscan, it’s an awesome tool and bugs happen to all of us!
[0]: https://github.com/firecat53/urlscan
[1]: https://github.com/firecat53/urlscan/issues/122
I'm not sure return codes are the source of your troubles...
It sounds our sensibilities are similar regarding cli and tool usage. This is a side note, but as someone who used to use "Bash strict mode" in all my scripts, I'm now a bit bearish on `set -e`, mainly due to the subtle caveats. If you're interested, the link below has a nice (and long) list of potentially surprising errexit gotchas:
https://mywiki.wooledge.org/BashFAQ/105
(The list begins below the anecdote.)
I'm really interested. What are their arguments? And how do they handle errors?
I think the idea is you use set -e during development to find where you should catch errors, but in production you may want it off to reduce strange side-effects (or explicitly check for success in the way you expect; so not that the command returned 0 but that the file it made exists and is the right length, etc).
If a command returns 0 when it didn't really do its job. Shouldn't we fix the command instead of the script?
Having -e set is to reduce strange side-effects, by having the script fail, instead of plowing headlong into the land of undefined/unexpected behavior.
> See https://mywiki.wooledge.org/BashFAQ/105 for examples of side effects.
The `if` bit should be well-known if you're writing bash. The pipe bit is unfortunate, and is why -o pipefail is recommended, too. Or, just writing in a sane language that isn't going to page you in the middle of the night.
[0]: https://wiki.archlinux.org/title/Systemd-nspawn
[1]: https://github.com/b0o/arch-lwc