Skip to content

Commit 75f49f5

Browse files
committed
Add documentation
1 parent 305220f commit 75f49f5

File tree

2 files changed

+202
-1
lines changed

2 files changed

+202
-1
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,14 @@ broken out into separate repositories.
2121
Libraries are documented either in-module or on a separate README in their
2222
respective folders
2323

24+
- `result` - friendly, exception-free value-or-error returns, similar to `Option[T]`, from [nim-result](https://github.com/arnetheduck/nim-result/)
25+
- `eh` - error-handling utils for working with tracked exceptions, `Result` or `Option` types. Please see [the dedicated docs](docs/error_handling.md).
2426
- `bitops2` - an updated version of `bitops.nim`, filling in gaps in original code
2527
- `byteutils` - utilities that make working with the Nim `byte` type convenient
2628
- `endians2` - utilities for converting to and from little / big endian integers
2729
- `objects` - get an object's base type at runtime, as a string
2830
- `ptrops` - pointer arithmetic utilities
2931
- `ranges` - utility functions for working with parts and blobs of memory
30-
- `result` - friendly, exception-free value-or-error returns, similar to `Option[T]`, from [nim-result](https://github.com/arnetheduck/nim-result/)
3132
- `shims` - backports of nim `devel` code to the stable version that Status is using
3233

3334
## Layout

docs/error_handling.md

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
## The `errors` pragma
2+
3+
The `errors` pragma is similar to the `raises` pragma, but it uses
4+
the Nim type system to ensure that the raised recoverable errors
5+
will be handled at the call-sites of the annotated function.
6+
7+
To achieve this, it performs the following simple transformation:
8+
9+
```nim
10+
proc foo(x: int, y: string): float {.errors: (ValueError, KeyError).} =
11+
body
12+
```
13+
14+
is re-written to the equivalent of:
15+
16+
```nim
17+
type
18+
Raising[ErrorsList, ResultType] = distinct ResultType
19+
20+
proc foo_original(x: int, y: string): float {.
21+
raises: [Defect, ValueError, KeyError]
22+
.} =
23+
body
24+
25+
template foo(x: int, y: string): untyped =
26+
Raising[(ValueError, KeyError), float](foo_original(x, y))
27+
```
28+
29+
Please note that the original proc now features a `raises` annotation
30+
that will guarantee that no other exceptions might be raised from it.
31+
The `Defect` type was implicitly added to the list as a convenience.
32+
33+
The returned distinct type will be useless at the call-site unless
34+
it is stripped-away through `raising`, `either` or `check` which are
35+
the error-handling mechanisms provided by this library and discussed
36+
further in this document.
37+
38+
If you accidentally forget to use one of the error-handling mechanisms,
39+
you'll get a compilation error along these lines:
40+
41+
```
42+
required type for x: float
43+
but expression 'Raising[(ValueError, KeyError), float](foo_original(x, y))' is of type: Raising[tuple of (ValueError, KeyError), system.float]
44+
```
45+
46+
Please note that if you have assigned the `Raising` result to a
47+
variable, the compilation error might happen on a line where you
48+
attempt to use that variable. To fix the error, please introduce
49+
error handling as early as possible at the right call-site such
50+
that no `Raising` variable is created at all.
51+
52+
`noerrors` is another pragma provided for convenience which is
53+
equivalent to an empty `errors` pragma. The forced error handling
54+
through the `Raising` type won't be applied.
55+
56+
Both pragmas can be combined with the `nodefects` pragma that
57+
indicates that the specific proc should be proven to be Defect-free.
58+
59+
The transformation uses a template by default to promote efficiency,
60+
but if you need to take the address the Raising proc, please add the
61+
`addressable` pragma that will force the wrapper to be a regular proc.
62+
63+
Finally, `failing` is another pragma provided for convenience which
64+
is equivalent to `{.errors: (CatchableError).}`.
65+
66+
## The `raising` annotation
67+
68+
The `raising` annotation is the simplest form of error tracking
69+
similar to the `try` annotation made popular by the [Midori error model](http://joeduffyblog.com/2016/02/07/the-error-model/),
70+
which is also [proposed for addition in C++](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0709r0.pdf) and [already available in Zig](https://ziglang.org/documentation/master/#try).
71+
72+
It merely marks the locations in the code where errors might be raised
73+
and strips away the `Raising` type to disarm the compiler checks:
74+
75+
```nim
76+
proc attachValidator(node: BeaconNode, keyFile: string) {.failing.} =
77+
node.addValidator(raising ValidatorPrivKey.init(raising readFile(keyFile))
78+
```
79+
80+
When applied to a `Result` or an `Option`, `raising` will use the
81+
`tryGet` API to attempt to obtain the computed value.
82+
83+
## The capital `Try` API
84+
85+
The capital `Try` API is similar to a regular `try` expression
86+
or a `try` statement. The only difference is that you must provide
87+
exception handlers for all possible recoverable errors. If you fail
88+
to do so, the compiler will point out the line in the `try` block
89+
where an unhandled exception might be raised:
90+
91+
```nim
92+
proc replaceValues(values: var openarray[string],
93+
replacements: Table[MyEnum, string]) =
94+
Try:
95+
for v in mitems(values):
96+
let
97+
enumValue = parseEnum v
98+
replacement = replacements[enumValue] # Error here
99+
v = replacement
100+
except ValueError:
101+
echo "Invalid enum value"
102+
```
103+
104+
The above example will fail to compile with an error indicating
105+
that `replacements[enumValue]` may fail with an unhandled `KeyError`.
106+
107+
## The `either` expression
108+
109+
The `either` expression can be used with APIs based on `Option[T]`,
110+
`Result[T, E]` or the `errors` pragma when it's appropriate to
111+
discriminate only between successful execution and any type of
112+
failure. Regardless of the error handling scheme being used,
113+
`either` is used like this:
114+
115+
```nim
116+
let x = either(foo(), fallbackValue)
117+
```
118+
119+
On success, `either` returns the successfully computed value
120+
and the failure side of the expression won't be evaluated at all.
121+
122+
Besides providing a substitute value, the failure side of the
123+
expression may also feature a `noReturn` statement such as
124+
`return`, `raise`, `quit` as long at it's used with the following
125+
syntax:
126+
127+
```nim
128+
let x = either foo():
129+
return
130+
```
131+
132+
Within the failure path, you can also use the `error` keyword to
133+
refer to the raised exception or the `error` value of the failed
134+
`Result`.
135+
136+
## The `check` expression
137+
138+
The `check` macro provides a general mechanism for handling
139+
the failures of APIs based the `errors` pragma or `Result[T, E]`
140+
where `E` is an `enum` or a case object type.
141+
142+
It takes an expression that might fail in multiple ways together
143+
with a block of error handlers that will be executed in case of
144+
failure. If the user failed to cover any of the possible failure
145+
types, this will result in a compilation error.
146+
147+
On success, the `check` returns the successfully computed value
148+
of the checked expression. In case of failure, the appropriate
149+
error handler is executed. It may produce a substitute value or
150+
it may return from the current function with a `return`, `raise`,
151+
`quit` or any other `noReturn` API.
152+
153+
The syntax of the `check` expression is the following:
154+
155+
```nim
156+
let x = check foo():
157+
SomeError as err: defaultValue
158+
AnotherError: return
159+
_: raise
160+
```
161+
162+
If the `foo()` function was using the `errors` pragma, the
163+
above example will be re-written to:
164+
165+
```nim
166+
let x = try:
167+
raising foo()
168+
except SomeError as err:
169+
defaultValue
170+
except AnotherError:
171+
return
172+
except CatchableError:
173+
raise
174+
```
175+
176+
Alternatively, if `foo()` was returning a `Result[T, E: enum]`, the
177+
example will be re-written to:
178+
179+
```nim
180+
let x = foo()
181+
if x.isOk:
182+
x.get
183+
else:
184+
case x.orror:
185+
of SomeError:
186+
let err = x.error
187+
defaultValue
188+
of AnotherError:
189+
return
190+
else:
191+
raiseResultError x
192+
```
193+
194+
The `Result` error type can also be a case object with a single `enum`
195+
discriminator that will be considered the error type. The generated code
196+
will be quite similar to the example above.
197+
198+
Please note that the special default case `_` is considered equivalent
199+
to `CatchableError` or `else` when working with enums.
200+

0 commit comments

Comments
 (0)