Skip to content

Commit 45d2ca8

Browse files
authored
Merge pull request #66 from input-output-hk/mgalazyn/adr-better-call-stacks-in-io
Add ADR: better call stacks of io exceptions
2 parents 0d50087 + f2e945d commit 45d2ca8

File tree

1 file changed

+90
-0
lines changed

1 file changed

+90
-0
lines changed
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# Status
2+
3+
✅ Accepted 2025-03-11
4+
5+
# Context
6+
7+
This ADR enriches exceptions thrown from `IO` functions with call stacks up to the function usage site.
8+
9+
## The problem
10+
This ADR bases on [[ADR-8-Use-RIO-in-cardano‐cli]].
11+
12+
The goal of this ADR is to provide better call stacks in `IO` exceptions.
13+
The problem with exceptions thrown from IO monad is that they do not carry call stack from where they were thrown.
14+
This is not very convenient when for example doing multiple file access or network operations in a row.
15+
You won't know which one has thrown an exception: you will have only a vague exception message without many details, for example:
16+
```
17+
Network.Socket.recvBuf: resource vanished (Connection reset by peer)
18+
```
19+
20+
## Solution
21+
22+
The currently used type alias adds call stacks to the functions:
23+
```haskell
24+
type CIO e a = HasCallStack => RIO e a
25+
```
26+
We can make sure that `IO` exceptions thrown in `IO` actions are captured and rethrown with a helper `runIO` function, meant to be used instead of `liftIO`:
27+
28+
```haskell
29+
import UnliftIO.Exceptions (catchAny, throwIO)
30+
31+
runIO :: IO a -> CIO a
32+
runIO m = withFrozenCallStack $ catchAny (liftIO m) (throwIO . mkE)
33+
where
34+
mkE :: SomeException -> IoeWrapper
35+
mkE e = withFrozenCallStack $ IoeWrapper e
36+
37+
-- | An exception wrapper type, adding a call stack to it
38+
data IoeWrapper = HasCallStack => IoeWrapper SomeException
39+
40+
deriving instance Show IoeWrapper
41+
42+
instance Exception IoeWrapper where
43+
displayException (IoeWrapper (SomeException e)) =
44+
constructorName <> ": " <> displayException e <> "\n" <> prettyCallStack callStack
45+
where
46+
constructorName = tyConName . typeRepTyCon $ typeOf e
47+
```
48+
The provided `runIO` wraps synchronous exceptions into `IoeWrapper`, which adds additional information about call stack.
49+
50+
The resulting call stack will contain entries up to the point where `runIO` was called.
51+
This means that in the following code:
52+
```haskell
53+
foo :: CIO e ()
54+
foo = do
55+
someFunction
56+
runIO $ do
57+
someOtherFunction
58+
someExceptionThrowingFunction
59+
```
60+
the exception thrown from `someExceptionThrowingFunction` and wrapped using `runIO` will only point to the place where `runIO` was called.
61+
62+
# Decision
63+
64+
The ADR gets adopted in `cardano-api` and `cardano-cli`.
65+
66+
Performance impact of the changes needs to be investigated.
67+
`cardano-cli` does not perform any expensive operations and it is a short-lived process, so the performance impact there is not that significant.
68+
However, in case of `cardano-api` the performance impact has to be investigated further, with the emphasis on the following areas:
69+
70+
* Transaction construction/fee balancing
71+
* Serialization
72+
* Key generation
73+
* Queries
74+
75+
This means that `HasCallStack` constraint should not be blindly put everywhere but only in places where it can aid debugging significantly.
76+
77+
# Consequences
78+
79+
1. More detailed call stacks when interacting with IO.
80+
81+
1. **A need for manual step when writing new code**.
82+
Developers would have to remember to use `runIO` instead of `liftIO`.
83+
84+
1. **Performance impact**.
85+
Including `HasCallStack` has [some small performance penalty][hascallstack-perf-penalty], so it may affect loops executed many times inside `CIO e`.
86+
87+
88+
[hascallstack-perf-penalty]: https://stackoverflow.com/questions/57471398/how-does-hascallstack-influence-the-performance-of-a-normal-branch-in-haskell
89+
90+
[modeline]: # ( vim: set spell spelllang=en: )

0 commit comments

Comments
 (0)