Capturing call stack with Haskell exceptions
Recently I discovered a nice way to capture call stack in Haskell exceptions almost transparently, and I’m going to share it in this post
If this is a known technique, let me know, otherwise — enjoy using it.
What is a call stack
Suppose somewhere in the program you use error
to signal an impossible situation:
foo :: [a] -> a
= error "impossible!"
foo [] :_ = a
foo a
bar :: [Int] -> [Int] -> Int
= foo a + foo b bar a b
And then accidentally use it with an empty list:
> bar [] []
λ*** Exception: impossible!
CallStack (from HasCallStack):
error, called at stacks.hs:4:10 in main:Main
Obviously, there is an error, but GHCI
also prints a peculiar thing: a call stack! It contains only one entry and isn’t helpful, though… So you go to GHC manual to see what HasCallStack
the message is talking about, and there it is: HasCallStack
section.
As the manual says, you add HasCallStack
constraint to your foo
and bar
functions:
foo :: HasCallStack => [a] -> a
= error "impossible!"
foo [] :_ = a
foo a
bar :: HasCallStack => [Int] -> [Int] -> Int
= foo a + foo b bar a b
Now the output becomes much more informative:
> bar [] []
λ*** Exception: impossible!
CallStack (from HasCallStack):
error, called at stacks.hs:6:10 in main:Main
.hs:10:11 in main:Main
foo, called at stacks<interactive>:5:1 in interactive:Ghci1 bar, called at
You get function names, module names and even source locations for all calls starting from the ghci
prompt down to the point where error
is called.
Remember, however, that stack is captured only as far from the error
call as there are HasCallStack
constraint. E.g., dropping the constraint from foo
will also exclude bar
from the log:
> bar [] []
λ*** Exception: impossible!
CallStack (from HasCallStack):
error, called at stacks.hs:6:10 in main:Main
Still, you get to know the precise location of the error
call, which is nice.
Caveat: head
, tail
, read
and so on use errorWithoutStackTrace
(for performance reasons), so you won’t ever see stack traces from them. One more reason to avoid head
!
Using exceptions
However, using error
to report errors is not very convenient: you can pass only a String
as an argument and so catching specific errors while propagating others becomes very hard and messy.
Fortunately, there is another mechanism in GHC for that: exceptions. So you define your custom exception type and throw it from the foo
function like this:
data FooException = FooException
deriving (Show, Exception)
foo :: HasCallStack => [a] -> a
= throw FooException
foo [] :_ = a
foo a
bar :: HasCallStack => [Int] -> [Int] -> Int
= foo a + foo b bar a b
You run it and expect to see the nice exception with a stack trace. But…
> bar [] []
λ*** Exception: FooException
You get none! What is going on, and how error
is different from throw
?
The reason is that exceptions don’t capture the stack trace automatically, even when thrown from a place with HasCallStack
context. There is an open issue to do so, reported back in 2016, but no progress was made yet.
Capturing stacks
But what if we want to capture stack with exceptions? One possible way would be to save the stack (represented as CallStack
type) as part of the exception constructor, then make your custom throwWithStack :: HasCallStack => Foo -> IO ()
function and use it everywhere, but that is too cumbersome, and you may just forget to use the right throwing function.
Fortunately, there is a better way. Recall that magic HasCallStack
constraint captures call stack from the point where something annotated with it is used. We don’t want to annotate throw
, but there is one more thing on the same line — exception constructor itself! It turns out, you can use GADTs to capture stack with an exception data:
data FooException where
FooException :: HasCallStack => FooException
And then access it in Show
instance:
instance Show FooException where
show FooException = "FooException\n" <> prettyCallStack callStack
deriving anyclass instance Exception FooException
-- alternatively, derive Show from stock and print call stack in 'displayException' method.
Here callStack
is provided by GHC.Stack
and will use HasCallStack
constraint introduced by pattern match on FooException
GADT constructor.
Let’s see an example of how it works:
> bar [] []
λ*** Exception: FooException
CallStack (from HasCallStack):
FooException, called at stacks.hs:18:16 in main:Main
.hs:22:11 in main:Main
foo, called at stacks<interactive>:7:1 in interactive:Ghci1 bar, called at
For another example, here is a real call stack I reproduced in our production code:
Exception: Operation timeout
CallStack (from HasCallStack):
TimeOut, called at src/Database/Bolt/Connection.hs:38:36 in hasbolt-0.1.4.3-inplace:Database.Bolt.Connection
/XXX/DB/Impl.hs:42:43 in xxx-0.3.5.0-inplace:XXX.DB.Impl
run, called at src/XXX/DB/Impl.hs:124:14 in xxx-0.3.5.0-inplace:XXX.DB.Impl
runDB, called at src/XXX/API/Program.hs:33:17 in xxx-0.3.5.0-inplace:XXX.API.Program programs, called at src
Final remarks
HasCallStack
is a magic constraint, so the fact that this trick works may or may not be a coincidence: some later change in GHC may stop GADT pattern match from affecting how HasCallStack
is solved. However, I think that this approach is useful enough and may be used in practice. Just don’t forget to add enough HasCallStack
to places which can fail.
Don’t forget, though, that HasCallStack
is not free and sometimes can break some optimizations, especially if used in recursive functions (that’s the reason head
& friends do not capture the stack).
Of course, this post does nothing to help debugging standard exceptions, like IOError
. For that, the usual way is to build with -prof
and run your code with +RTS -xc
, as documented in the manual.