exitFailure
doesn’t exit– nor do exitSuccess
, exitWith
and die
Haskell’s base
library contains
System.Exit.exitFailure
.
From the name, it sounds as though running it ought to cause your
Haskell program to exit (with a failure exit code). But it doesn’t,
at least not directly. Instead it throws an exception of type
ExitCode
(specifically, ExitFailure 1
). Then it is the responsibility of
some exception handler to catch that exception and cause the program
to exit appropriately. What handler is that? It is
real_handler
in GHC.Internal.TopHandler
. It has a special case for ExitCode
exceptions:
case fromException se of
Just ExitSuccess -> exit 0
Just (ExitFailure n) -> exit n
exit
ultimately calls the runtime system (RTS) function
shutdownHaskellAndExit
,
which causes the RTS to exit and thus the program to terminate.
Why then, instead of exiting by throwing an exception, don’t we just
expose that exit
function and call it directly? The documentation
for
exitWith
explains:
As an
ExitCode
is anException
, it can be caught using the functions ofControl.Exception
. This means that cleanup computations added withbracket
(fromControl.Exception
) are also executed properly onexitWith
.
Using the exception system to trigger program exit allows resources
that were allocated in a bracket
, but are not managed by the RTS,
such as files on disk, to be cleaned up before program exit. That’s
useful. But exceptions-for-exit also has strange consequences: if the
ExitCode
exception is caught then the program won’t exit!
If a
try
block catches ExitCode
, and the body ends up calling one of the exit
functions then the program will not exit. The correct workaround is
to not do that: don’t catch ExitCode
exceptions (and don’t catch
SomeException
either, which is the “catch all” exception).
@ExitCode $ do
try ...
exitFailure
Similarly, if
forkIO
spawns a child thread and the child thread ends up calling one of the
exit functions then forkIO
swallows the exception and the program
will not exit. The following code misleadingly prints main: ExitFailure 1
(as forkIO
catches, prints, and discards the
ExitCode
exception) and then ends up looping forever in
threadDelay
s. That’s probably not what we wanted! A workaround is
to use the async
package
rather than forkIO
. async
provides structured concurrency
primitives that are harder to misuse. In particular, when a thread
forked by async
terminates with an exception then async
rethrows
the exception to the parent thread.
$ do
forkIO ...
exitFailure$
forever 1000 * 1000) threadDelay (
So exitFailure
doesn’t exit, it throws an exception. The exception
will cause the program exit if it gets to the top of the main thread.
If it doesn’t, it won’t, so some care is needed!