Skip to content

hGetContents does not close handle on exception #712

Description

@julmb

The documentation for Data.Text.IO.hGetContents promises

The 'Handle' is closed once the contents have been read, or if an exception is thrown.

However, this seems to not be the case:

import Control.Exception
import Data.Text.IO qualified as T
import System.IO

main :: IO ()
main = do
    handle <- openFile "/bin/bash" ReadMode
    result <- try @IOException $ T.hGetContents handle
    closed <- hIsClosed handle
    print handle >> print result >> print closed

On my system (ghc-9.12.2, text-2.1.4), this yields

{handle: /bin/bash}
Left /bin/bash: hGetContents: invalid argument (cannot decode byte sequence starting from 224)
False

As far as I can tell, this also affects Data.Text.IO.readFile, which uses openFile instead of withFile and so the handle is not closed there either. System.IO.readFile' uses withFile and has a comment acknowledging that this should not be necessary:

There's a bit of overkill here—both withFile and hGetContents' will close the file in the end.

I cannot say with 100% certainty, but I believe this to be the root cause of a "resource exhausted (Too many open files)" exception in my application.

Activity

Bodigrim commented on Sep 26, 2026

@Bodigrim
Contributor

There is no promise that the handle will be closed promptly, only that it will be closed eventually (namely, when the garbage collector will trigger finalisers).

But I agree that Data.Text.IO.readFile should use withFile instead of openFile. Patches are welcome.

julmb commented on Sep 26, 2026

@julmb
ContributorAuthor

Ah, I guess System.IO.hGetContents' also does not close the handle promptly. Is there a reason why both System.IO.hGetContents' and Data.Text.IO.hGetContents should not do so, given that they aim for non-lazy IO semantics? From what I can tell, this is what Data.ByteString.hGetContents does, but it is difficult to test since there is no easy way to provoke failure. I feel like the immediate closing behavior would be less surprising, but maybe there is a good reason for the way things are.

I guess this also means that the comment on System.IO.readFile' is not quite accurate. Using withFile is not overkill, as it ensures prompt closure of the handle which the underlying hGetContents' by itself does not guarantee.

Bodigrim commented on Sep 27, 2026

@Bodigrim
Contributor

I think I might have misled you above, I was not aware that hGetContents sets semi-closed mode.

I'm not an expert in this area: hGetContents almost immediately goes into withHandle and code there gets too dense to skim through. Perhaps the precise semantics in the presence of exceptions a good question for GHC issue tracker.


Anyway, Data.Text.IO.readFile should be modelled after System.IO.readFile' (using withFile) and not System.IO.readFile (which leaves it to hGetContents to close the handle). The current implementation is most likely a copy-paste without much analysis. Fancy to fire a PR?

Lysxia commented on Oct 6, 2026

@Lysxia
Contributor

We've fixed readFile but I think the behavior of hGetContents and hGetContents' here is a bug, or at the very least undesirable.

Looking at System.IO for comparison:

  • hGetContents closes the handle on decoding exception, which is technically an IOException, so this arguably respects its spec:

    A semi-closed handle becomes closed:

    • if hClose is applied to it;
    • if an I/O error occurs when reading an item from the handle;
    • or once the entire contents of the handle has been read.
  • hGetContents' does not close the handle on error. I believe this is a bug. I've opened a GHC ticket to get another opinion https://gitlab.haskell.org/ghc/ghc/-/work_items/27905

reopened this on Oct 6, 2026
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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions