Skip to content

main cannot set an exit status without writing to stderr, and IO.die codes above 255 wrap (256 exits 0) #1465

Closed

Description

@viridale

What Bend should do

  1. A main of type IO(U32) exits with its result:

    import Base
    
    def main() -> IO(U32):
      do IO<U32>:
        IO.print_err("error: unexpected argument '--frob' found")
        return 2

    This should print the line and exit 2, at the point where an IO(Unit) main exits today. Today it compiles, prints the line and exits 0, natively, in compiled JS and under bend status.bend: the result is silently dropped.

  2. An exit status outside 0..255, whether from IO.die or from main, should fail with a message and a non-zero status instead of wrapping. Today the OS wraps it modulo 256, so a failure can report success:

    import Base
    
    def main() -> IO(Unit):
      IO.die(Unit, 256, "x")
    $ ./big; echo $?
    x
    0
    

    The same happens with bun big.js and bend big.bend.

Why

Command-line programs signal errors through the exit status: 2 for a usage error, by convention, after a formatted message of several lines. In Bend today, the only way to exit non-zero is IO.die(code, msg), and it always writes msg plus a newline to stderr. #1101 confirms that newline is intended. So a program must either squeeze its whole message into msg, or print the message itself and then accept a stray blank line from IO.die(code, ""). It can never exit non-zero silently, for example after a child process has already reported the error.

The only workaround is a user-written foreign effect that calls exit(). A foreign def makes bend PROOF.bend fail for every law in the import graph (#1463).

Prior art: C int main, Rust fn main() -> ExitCode.

bend 2.0.36 · Darwin arm64 · bun 1.2.23

Activity

  1. Lorenzobattistela commented on Oct 11, 2026

    @Lorenzobattistela
    Collaborator

    Thanks for the report. We closed #1474 because we don't want main's return value to become the exit status, since that changes the meaning of every existing IO(U32) main.

    The direction we'd take instead:

    1. Add IO.exit(code: U32) -> IO(A), which exits with code and writes nothing to stderr. That covers the CLI case: print your own multi-line usage error, then IO.exit(2). You no longer need a foreign exit(), so bend PROOF.bend fails over foreign defs that no law depends on #1463 stops getting in the way too.
    2. Have IO.die and IO.exit check that the code is in 0..255. Today an out-of-range code wraps mod 256 (IO.die(256, "x") exits 0). It should fail with a nonzero status instead.

    main keeps working as it does now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions