Before you begin
Why this matters
A bracket that type-checks can still release too early, report the wrong failure, or let an error escape the try that was written for it. If you know what bracket protects, and what it cannot see, you can predict where your program fails and keep the cause visible.
What this helps you reason about
- Read a bracket call and say which argument runs first, second, and last.
- Predict whether a failure in a returned result is caught by the try around the bracket.
- Spot a release that hides the reason an action failed.
In Python, a with block closes a file when the block ends, and every value computed in the block is already computed. Haskell’s bracket also releases a resource when its action ends, after success or failure. But a Haskell value can be computed long after the line that seems to compute it, so a result can leave the bracket before its work is done. Cleanup on a failing path released with bracket_, and Resource cleanup and RAII tied a resource to one action with bracket. This lesson looks closely at bracket itself: its arguments, its result, and what happens to work and failures that come later.
Already know this? The short version
- bracket :: IO a -> (a -> IO b) -> (a -> IO c) -> IO c runs take, then use, then release; release and use both receive take’s result, release’s own result is dropped, and a swap of the last two arguments compiles when both give IO ().
- onException runs its undo only when the action throws, and bracketOnError releases only on failure, for a resource handed on to the caller; bracket_ and finally release on both outcomes.
- return x inside a bracket hands out a thunk that is computed after the release and outside any try around it; evaluate computes only weak head normal form, so force a list or a pair with force from Control.DeepSeq, or with length or sum.
- An exception from the release replaces the action’s exception. The section “What goes wrong with bracket in real programs?” covers readFile then writeFile on one file, hClose by hand, taking the resource before the bracket, and a release that fails after a good action.
Words in this lesson (8)
- resource
- Something a program takes for a while and must give back, such as a booking, a lock, or an open file. While one part of the program holds it, other parts cannot have it.
- cleanup
- The code that releases what a function took, such as giving back a booking or closing a file. It must run on every way out of the function.
- bracket
- A function from Control.Exception. bracket take release use runs take, passes its result to use, and then runs release on that result, even when use throws. The result of use is the result of the whole bracket.
- bracket_ and finally
- Functions from Control.Exception. bracket_ take release action runs take, then action, then release. action `finally` release runs release after action. Both run release even when action throws, and then pass the exception on.
- exception
- An error raised while a program runs, such as the one !! raises for a position past the end. Unless the program handles it, the program stops and prints its message.
- thunk
- GHC’s stored description of work that has not happened yet. When the value is needed, GHC runs the work and replaces the thunk with the result.
- weak head normal form
- A value computed as far as its outer constructor and no further, such as Just _ or (_, _) with the parts still waiting. A number in weak head normal form is fully computed.
- lazy I/O
- Reading that gives its text at once as a thunk and reads the file only when the text is needed, as hGetContents and readFile do. The file must still be open when the text is needed.
Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.
01 / Argument order
Which argument of bracket runs when?
bracket has three arguments, and their order is not the order in which they run. Its type, from Control.Exception, shows how they connect:
bracket :: IO a -> (a -> IO b) -> (a -> IO c) -> IO c
The first argument takes the resource and gives a value of type a, such as a kit number. The second is the release, and the third is the use. Both receive that value, so the release always gives back exactly what was taken. bracket runs the take, then the use, then the release. The use is written last, and the release, which runs last, is written in the middle.
GHC checks the types, not the names. If the release and the use have the same type, such as Int -> IO (), a call with the two swapped still compiles. It returns the kit and then records the night.
lendKit :: Int -> IO Int -- take: gives the kit
returnKit :: Int -> IO () -- release
recordNight :: Int -> IO () -- use
main = bracket (lendKit 4) returnKit recordNight
-- runs lendKit, recordNight, returnKit bracket take release use runs take, then use, then release; a swap of release and use compiles when both have the same type.
Coming from Python: with lend_kit(4) as kit: names the release inside lend_kit’s context manager. bracket takes the release as its own argument, before the body, and nothing stops you from passing the body there instead.
Precisely: why the release comes second
With the use last, a call can end with a lambda that runs over several lines: bracket take release (\kit -> do …). A with function such as withFile is bracket with the first two arguments filled in. bracket also masks exceptions from other threads while take and release run, so that one cannot arrive between taking the resource and starting the use.
Experiment
Swap the release and the use
bracket runs its first argument, then its third, then its second. It knows each argument only by its place, not by its name. When the release and the use have the same type, a swap is still a correct program, with the wrong order.
Why it matters Read a bracket call as take, release, use. The use comes last, and the type checker does not catch a swap when the release and the use have the same type.
Predict the three lines and their order. Then change the last line to main = bracket (lendKit 4) recordNight returnKit, and predict whether GHC accepts it and what it prints.
import Control.Exception
lendKit :: Int -> IO Int
lendKit bed = do
putStrLn ("lend kit to bed " ++ show bed)
return bed
returnKit :: Int -> IO ()
returnKit bed = putStrLn ("return kit from bed " ++ show bed)
recordNight :: Int -> IO ()
recordNight bed = putStrLn ("record night at bed " ++ show bed)
main :: IO ()
main = bracket (lendKit 4) returnKit recordNight
What to look for
GHCi shows one location per stop, not a stack of frames. The stepper stops in lendKit with bed = 4, then in recordNight with bed = 4, then in returnKit with bed = 4: both functions receive the number that lendKit returned. Output can appear in parts: lend kit to bed appears before the 4, which is computed when putStrLn needs it. The run prints lend kit to bed 4, record night at bed 4, return kit from bed 4. After the edit, GHC accepts the program, because recordNight and returnKit have the same type, Int -> IO (). The stepper stops in returnKit before recordNight, and the run prints lend kit to bed 4, return kit from bed 4, record night at bed 4.
Expected output
lend kit to bed 4
record night at bed 4
return kit from bed 402 / Outcomes
What does bracket give back on each outcome?
The result of the whole bracket is the result of the use, of type c. The release’s result, of type b, is thrown away. When the use throws, bracket runs the release and then throws the same exception again, so the caller sees it, for example as Left from try.
Control.Exception has a function for each rule about when to release:
- bracket and bracket_ release on both outcomes; bracket_ passes nothing to the action.
- action `finally` release does the same when there is no separate take.
- action `onException` undo runs undo only when action throws, then throws the exception again. Use it for work that must be undone after a failure and kept after success, such as a booking. In the code below, reserve and cancel stand for any such pair of actions.
- bracketOnError take release use releases only when use throws. Use it when use hands the resource on to the caller after success, such as a function that opens a file and returns its Handle.
-- keep the booking if the night works, cancel it if not
bookBed bed = do
reserve bed
recordNight bed `onException` cancel bed bracket gives the use’s result and passes the use’s exception on; choose bracket_, finally, onException, or bracketOnError by when the release must run.
03 / Lazy results
When is the result of the action computed?
bracket protects what happens while the use runs. In Haskell, a value is computed when it is needed, not when its line runs. return x gives x as it is, and x may still be a thunk.
So a use that ends with return (rate 840 minutes) finishes at once. bracket sees success and runs the release. try around the bracket gives Right with the thunk. Later, print needs the number, and only then does the division run. If minutes is 0, the division by zero happens there, outside the bracket and outside the try. Haskell Maybe, Either, and partial functions showed that a partial function fails where its result is first needed; here that place is after the cleanup.
evaluate x, from Control.Exception, is an IO action that computes x when it runs. Written as the last action of the use, it makes the work, and its failure, happen inside the bracket. Resource cleanup and RAII used it to count lines while a file was open; it matters for any result, not only for results that read a file.
night minutes =
bracket (lendKit 4) returnKit (\_ ->
evaluate (rate 840 minutes)) -- computed here, inside A result returned from a bracket is computed later, outside it; compute it with evaluate inside, so its failure happens where bracket and try can see it.
Coming from Java: A value returned from a try-with-resources block is already computed when close() runs. In Haskell, return hands out a thunk, so the work can happen after the release.
Experiment
Catch a failure that has not happened yet
return x gives x as it is, and x is a thunk until something needs it. The bracket ends, the kit is returned, and try gives Right with the thunk. print needs the number later, and only then does the division run and fail. evaluate x computes x while its action runs, so the failure happens inside the bracket and try catches it.
Why it matters A result returned from inside a bracket is computed later, outside it. Compute it with evaluate inside, so its failure happens where the bracket and try can see it.
No minutes were recorded, so rate divides by zero. Predict the three lines. Then change evaluate (rate 840 minutes) to return (rate 840 minutes) on line 17, and predict what try gives.
import Control.Exception
lendKit :: Int -> IO Int
lendKit bed = do
putStrLn ("lend kit to bed " ++ show bed)
return bed
returnKit :: Int -> IO ()
returnKit bed = putStrLn ("return kit from bed " ++ show bed)
-- Breaths per minute, from breaths counted and minutes recorded.
rate :: Int -> Int -> Int
rate breaths minutes = div breaths minutes
night :: Int -> IO Int
night minutes =
bracket (lendKit 4) returnKit (\_ -> evaluate (rate 840 minutes))
main :: IO ()
main = do
result <- try (night 0)
print (result :: Either ArithException Int)
What to look for
The stepper stops in night with minutes = 0, then in lendKit. After a stop back on line 17, GHCi stops in rate, on line 13, with breaths = 840 and minutes = 0: the division runs inside the bracket, before returnKit. Then the stepper stops in returnKit, and on line 22 result shows Left DivideByZero. The run prints lend kit to bed 4, return kit from bed 4, Left divide by zero. After the edit, there is no stop in rate before returnKit. On line 22, result shows Right _ (unevaluated): try saw the action end normally. The output shows Right before the number, then GHCi stops in rate, and the run ends with *** Exception: divide by zero. Neither the bracket nor the try was still running when the division failed.
Expected output
lend kit to bed 4
return kit from bed 4
Left divide by zero04 / How far evaluate goes
How much of a value does evaluate compute?
evaluate computes its argument to weak head normal form, the outer constructor only. For a number, that is the whole value. For a list, it is the first cell: the first element and the rest of the list may still be thunks. A Maybe or a pair stops at Just or at (_, _) in the same way.
With lazy I/O, that matters. hGetContents gives the text of a file as a thunk. evaluate (lines text) reads enough to build the first cell, and the rest of the list still needs the file. After withFile closes it, the next read fails with hGetContents: illegal operation (delayed read on closed handle).
To finish the work inside the bracket, compute everything the result needs. force, from Control.DeepSeq in the deepseq package that comes with GHC, computes a value all the way down, so evaluate (force xs) finishes the whole list. evaluate (length xs) finishes the list’s cells, and evaluate (sum xs) also computes each number.
import Control.DeepSeq (force)
readNight :: Handle -> IO [String]
readNight file = do
text <- hGetContents file
evaluate (force (lines text)) -- the whole list, while open evaluate computes only the outer constructor; use force, length, or sum to finish everything the result needs from the resource.
Precisely: NFData and hGetContents’
force needs an NFData instance for the type, which deepseq provides for lists, Maybe, pairs, numbers, and Char; for your own data type, derive it with Generic or write one. hGetContents’, in System.IO since base 4.15 (GHC 9.0, the stepper’s compiler), reads the whole text at once, so its result does not need the file.
Experiment
Evaluate a list that still needs its file
evaluate computes its argument to weak head normal form. For a list, that is the first cell: (:) with its two parts still thunks. The rest of the list still depends on reading the file. force computes the whole list, every cell and every element, so the reading finishes while withFile holds the file open.
Why it matters evaluate on a list, a pair, or a Maybe computes only the outer constructor. Force everything the result needs from the resource.
readNight uses evaluate, so it looks safe. Predict whether length readings works after withFile has closed the file. Then add import Control.DeepSeq (force) as a third import line, change evaluate (lines text) to evaluate (force (lines text)), and predict again.
import Control.Exception
import System.IO
-- One oxygen reading per line, as the sensor kit wrote them.
readNight :: Handle -> IO [String]
readNight file = do
text <- hGetContents file
evaluate (lines text)
main :: IO ()
main = do
writeFile "night.txt" "97\n95\n91\n"
readings <- withFile "night.txt" ReadMode readNight
result <- try (evaluate (length readings))
print (result :: Either IOException Int)
What to look for
The first line of main writes the file that the program reads. In readNight, file shows GHC.IO.Handle.Types.FileHandle "night.txt" _ (unevaluated), and text shows _ (unevaluated). Back in main, on line 14, readings shows _ (unevaluated) : _ (unevaluated): one list cell, whose first element and whose rest are both still waiting. That is all evaluate computed. length then needs the rest, which needs the closed file, and the run prints Left night.txt: hGetContents: illegal operation (delayed read on closed handle). After the edit, the lines below the imports move down by one. On line 15, readings shows ["97","95","91"], and the run prints Right 3.
Expected output
Left night.txt: hGetContents: illegal operation (delayed read on closed handle)05 / A failing release
What happens when the release itself fails?
The release is code too, and it can throw: a log that is full, a network that is down. If the use has already thrown, bracket runs the release while that first exception waits. When the release throws, its exception leaves bracket, and the first one is lost. The caller sees clinic log full and never learns that the sensor came loose.
One fix is to catch the release’s own failure inside the release, with try, as below, where saveToLog stands for the release’s own work. Then the release’s failure cannot replace the use’s exception. That keeps the reason the use failed. Look at what it drops before you use it: after a good night, the release’s failure is then silent too.
returnKit bed = do
result <- try (saveToLog bed)
case result of
Left e -> putStrLn ("log failed: " ++ show (e :: IOException))
Right () -> return () An exception from the release replaces the exception from the use; a release that can fail can hide why the use failed.
Coming from Java: try-with-resources keeps the body’s exception and attaches the exception from close() to it as suppressed. bracket keeps only the release’s.
Experiment
Lose the reason a night failed
bracket catches the exception from recordNight, runs the release, and then throws the same exception again. When the release throws first, that exception leaves bracket instead, and the first one is lost.
Why it matters An exception from the release replaces the exception from the action. A release that can fail can hide why the action failed.
Both the night and the release fail. Predict which reason try gives. Then delete line 11, the throwIO in returnKit, and predict again.
import Control.Exception
lendKit :: Int -> IO Int
lendKit bed = do
putStrLn ("lend kit to bed " ++ show bed)
return bed
returnKit :: Int -> IO ()
returnKit bed = do
putStrLn ("return kit from bed " ++ show bed)
throwIO (userError "clinic log full")
recordNight :: Int -> IO ()
recordNight bed = throwIO (userError "sensor came loose")
main :: IO ()
main = do
result <- try (bracket (lendKit 4) returnKit recordNight)
print (result :: Either IOException ())
What to look for
The stepper stops in lendKit, then in recordNight on line 14. No variables are listed there: GHCi lists only the names the expression uses, and the throwIO line does not use bed. Next, the stepper stops in returnKit on lines 9 to 11, so the release runs after the failure. On line 19, result shows Left _ (unevaluated). The output shows Left before the message, and GHCi stops on line 11 again while the message is printed: it is the release’s exception. The run prints lend kit to bed 4, return kit from bed 4, Left user error (clinic log full). After the edit, the last stop is in recordNight, and the run prints Left user error (sensor came loose).
Expected output
lend kit to bed 4
return kit from bed 4
Left user error (clinic log full)06 / In production
What goes wrong with bracket in real programs?
Four resource bugs are common in real Haskell programs.
readFile, then writeFile to the same file. readFile is lazy I/O: the file stays open until its text has been read to the end. Writing the file before that fails with night.txt: openFile: resource busy (file is locked), because GHC allows many readers of a file or one writer, not both at once. Finish the text first, with force or length, or read with withFile.
hClose by hand after hGetContents. text <- hGetContents h, then hClose h, then lines text, fails with the delayed read error from the section above. The close was written in the right place for a strict language.
Taking the resource before the bracket. h <- openFile …, then another action, then bracket (return h) hClose use: if the action between them throws, nothing closes h. Make the take the first argument of bracket, or use withFile.
A release that fails after a good use. hClose writes out output that is still buffered, so on a full disk a write handle’s close can fail after every write seemed to work. Its exception is the only sign that the data is lost. In the code below, hPutStrLn file writes a line to the file. Catching every failure of a release, as in the section above, hides it.
-- the data is lost if this release fails:
withFile "night.txt" WriteMode (\file ->
hPutStrLn file "97") Finish lazy text before the file is used again, take the resource inside bracket, and let a release that can lose data report its failure.
07 / Try it
Can you predict what try catches?
Predict each line. Then say where the division by zero would happen if main printed second.
import Control.Exception
lendKit :: String -> IO String
lendKit kit = do
putStrLn ("lend " ++ kit)
return kit
returnKit :: String -> IO ()
returnKit kit = putStrLn ("return " ++ kit)
-- Share of the night spent in deep sleep, in percent.
deepShare :: Int -> Int -> Int
deepShare deep hours = div (100 * deep) hours
night :: String -> Int -> IO Int
night kit hours =
bracket (lendKit kit) returnKit (\_ -> return (deepShare 2 hours))
main :: IO ()
main = do
first <- try (night "kit A" 8)
print (first :: Either ArithException Int)
second <- try (night "kit B" 0) :: IO (Either ArithException Int)
putStrLn (either (const "caught") (const "no failure yet") second)
What does the program print, in order?
Reveal the reasoning
lend kit A, return kit A, Right 25, lend kit B, return kit B, no failure yet. The use ends with return, so bracket returns each kit before anything computes deepShare. For kit A, print needs the number after the release, and 200 divided by 8 is 25. For kit B, try sees the use end normally and gives Right with a thunk; either looks only at Right and never needs the number. If main printed second, the division by zero would happen at that print, outside the bracket and outside the try, and the program would stop.
Practice tickets
- HS-901 · The station log shows defibrillators back before each call → Free
- HS-902 · The dialysis screen stops when it shows the treatment minutes → Sign in · Pro
- HS-903 · Staff see “upload failed” when a heart monitor came loose → Sign in · Pro