What to do about `static func main() throws`?

There’s still some benefit to knowing which line of main caused the error in a lot of simple programs, though, especially those that linearly go through multiple steps and then exit.

1 Like

there are three choices of things you could potentially log when encountering an error:

  1. the main function itself
  2. the location inside the main function where the error was propogated
  3. the actual content of the error that was thrown

to me, even if backtrace collection was instantaneous and had no possibility of causing cascade failures (it does), option 3 seems vastly preferable to either 1 or 2.

1 Like

Maybe we can treat top-level code specially, but I'm not sure we could get away with it for a main method on a @main type, at least not without emitting the function twice. main is still also just a regular function that other code in the program could call expecting to catch and handle errors from, so it can't unilaterally trap at every point it would propagate an error.

1 Like

I guess the basic question here is: Are errors thrown from main/top level code intended for developers or for users.

Personally, I agree that not printing the trace would be the right move, printing a localized description and defining a protocol like @benlings mentioned would be good as well.

If one wants to treat any particular throwing location in main as a "to be handled by devs" kinda thing, you can always stick a ! on the try.

Yeah, if the top-level handler isn't at least trying to print the error value before it crashes, that seems like a bug.

This thread seems to have moved past the idea that an error thrown out of main should ultimately just cause an exit(1), and I'm not sure I understand the reasoning behind that.

3 Likes

Maybe it should do exit(1). That isn't what happens at present, though. (And, to be clear, the backtracer had nothing to do with that; the current behaviour is that the program crashes using _assertionFailure(). It just so happens that the backtracer exposed that.)

1 Like

Could there also be an issue with buffered output, like maybe the handler was printing the error previously but the new backtrace handling takes control of the process before the message can make it to stdout?

Yeah, if the default handler for top-level errors was that these are expected error paths, it would be distinct enough from try! that (hopefully) it would be an actual choice people could make, rather than basically a toss-up until you need backtrace information.

1 Like

Let me make my position clear:

  • Programmers have total control over what their program does when it fails by throwing an error, via the rather obvious mechanism of just catching the error themselves and doing whatever they want to do.
  • We could just force programs to always do that. The reason we don't is just convenience for the programmer. However, to not be a false convenience, it needs to do something that most programmers would actually find tolerable.
  • I do not think that crashing and spitting out a backtrace from some random place in the runtime meets that standard. The crash means the exit is often unnecessarily slow, and while it produces extra information, that information is useless in practice, even to the programmers. And the extra information looking like debug output means it isn't something that programmers should be exposing to normal users.
  • In short, we're behaving as if the thrown error is a programmer mistake rather than just a dynamic failure, which is out of keeping with our overall philosophy for error-handling in Swift, which is that programmer mistakes should cause immediate crashes and error-handling should be reserved for dynamic failures.
  • A more reasonable default would be to present the thrown error to the user in some well-documented (and localized if possible) way and then report that the process failed.
  • We do not need to allow any customization of that, because the right way for the programmer to customize it is just to catch the error themselves.
  • Return codes are next to useless for information flow beyond success/failure. Treating a thrown error as an expected failure and exiting with 1 is a completely reasonable choice. If someone really cares about setting a return code of exactly 7, they can catch the error themselves and call exit.

So I think we should just change the default handler to print the error, localized if possible, to stderr and then exit(1).

18 Likes

No, it's nothing like that. The backtracer only takes control because we executed a trap instruction (i.e. crashed). If we'd exited with e.g. exit(1), it wouldn't have kicked in, and we've already crashed at that point — so there couldn't have been any flushing happening either.

There are examples of UNIX programs that return useful information in their exit status, and it's totally accessible from the shell (in $?), so while it's true that 0 and 1 are by far the most common return codes, they aren't the only ones.

That said, I don't disagree with what you're saying and I think returning 1 is probably a reasonable default unless/until we have some other mechanism to associate a specific exit code with an Error.

1 Like

Yes, I'm aware that there are a handful of programs that do that. It is very uncommon, though, and generally uses a program-specific protocol. I don't think it's worth putting any extra effort into supporting beyond exposing the exit function from the System library.

That ship sailed when SWIFT_BACKTRACE started defaulting to enabled. Now, throwing from main produces an unhandled error backtrace. I assume this is an intentional decision, given that it required adding a special behavior to the error-catching wrapper that invokes main. Reverting that behavior and just returning 1 would be a regression, IMO.

And even if the default were switched to off (like it is on macOS), there’s still the question of how the backtrace should behave when the feature is enabled.

This is untrue. To re-iterate, the new backtracing code has not changed the behaviour here, and to be more specific, no changes were made to the error-catching wrapper that invokes main. It has just exposed the fact that the existing behaviour is to fail with an assertion failure, which crashes the program.

It seems that some people were under the impression that throwing from main caused the process to do an exit(1), but this appears not to be the case.

well, before 5.9, it was kind of like that, from the perspective of whatever was running the application. if it returned from main, you got a 0, if it threw from main, you got something that was not a 0, possibly even a 1. the latter came with a lot of unsymbolicated noise printed to the terminal, but it was still useful for tooling that read the return codes, like CI runners.

2 Likes

Yeah, I don't see anything about the current behavior that it would be a "regression" to lose.

3 Likes

Agreed. The backtraces we're handing out here are pretty pointless. The backtrace we used to generate was likewise pointless. exit(1) is a much better plan.

2 Likes

Yeah, neither the summary crash we did before nor the backtrace we do now is really within the bounds of what I'd consider to be "defined behavior" from the Swift program's point of view. Although Wadler's law is a thing, we should be OK in principle expanding the definition of Swift behavior to guarantee a graceful non-zero exit if you throw from main.

Fair enough; if this wasn’t an intentional behavior change, I have no strong opinion of my own.