Skip to content

Commit 7bb6e67

Browse files
committed
Error handling.
1 parent c625533 commit 7bb6e67

File tree

6 files changed

+600
-3
lines changed

6 files changed

+600
-3
lines changed

docs/lib/overview/errors.rst

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
Error handling
2+
==============
3+
4+
leet offers utilities to deal with two groups of errors: *recoverable* and *unrecoverable*.
5+
6+
An error is recoverable when it can be treated by your code and execution is allowed to continue.
7+
For example if you're making a `REPL <https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop>`_, getting a wrong instruction is not the end of the world: your code realizes it can't execute it, lets the user know, and waits for the next instruction.
8+
9+
An error is unrecoverable when it is impossible to continue execution after it happens.
10+
For example if your code tries to access the 100th element of a slice that only has capacity for 10 elements, execution cannot continue.
11+
12+
.. note::
13+
14+
- The separation between recoverable and unrecoverable errors is inspired by `Rust's error handling <https://doc.rust-lang.org/book/ch09-00-error-handling.html>`_.
15+
- The error handling system is inspired by `GLib's error handling <https://docs.gtk.org/glib/error-reporting.html>`_.
16+
17+
Unrecoverable errors
18+
--------------------
19+
20+
When you realize something went horribly wrong and your program cannot continue, you should exit with a panic.
21+
A panic **must** be triggered with a readable message describing the error.
22+
23+
Using our slice example from before:
24+
25+
.. code-block:: c
26+
:caption: Safe slice access that triggers a panic if the index is out of bounds.
27+
:emphasize-lines: 7
28+
29+
void*
30+
slice_sat(struct slice* p, size_t idx)
31+
{
32+
size_t offset = p->_el_size * idx;
33+
34+
if (offset >= p->_capacity)
35+
panic("access out of bounds: slice capacity is %u but offset was %u.", p->_capacity, offset);
36+
37+
return p->_data + offset;
38+
}
39+
40+
The panic will print your message to :code:`stderr` along with where it happened, and exit with a non-zero code.
41+
42+
.. code-block:: text
43+
:caption: Panic location and readable error message.
44+
45+
panicked at include/ds/slice.h:190:
46+
slice access out of bounds: slice capacity is 10 but offset was 100.
47+
48+
Recoverable errors
49+
------------------
50+
51+
Reporting errors
52+
________________
53+
54+
When a function fails with an error that does not require stopping execution, you **may** report the error to the caller so it can be handled.
55+
It is equivalent to a throw in languages that have exceptions.
56+
Functions that can report a recoverable error **should** accept a handle to an error pointer as the last parameter.
57+
58+
Using our REPL example:
59+
60+
.. code-block:: c
61+
:caption: Binary operation that reports an error if there is only one operand.
62+
:emphasize-lines: 6
63+
64+
void
65+
bin_op(struct slice* s, char op, struct error** error)
66+
{
67+
if (s->_ptr < s->_el_size * 2)
68+
{
69+
error_set(error, LINVAL, "%c is a binary operation but the stack only has one value.", op);
70+
}
71+
else
72+
{
73+
// We have at least two values, execute the operation.
74+
// ... snip ...
75+
}
76+
}
77+
78+
.. attention::
79+
80+
Reporting an error allocates memory and transfers its ownership to the caller.
81+
That means the caller **must not** allocate memory for the report, but **must** free the memory after handling it.
82+
83+
Handling errors
84+
_______________
85+
86+
To get error reports you **must** pass a valid pointer to a function's :code:`error` parameter.
87+
Calling a function with a valid error pointer and checking if the pointer was set is equivalent to a try-catch in languages that support exceptions.
88+
The error pointer will be set if the function encounters an error, or *left unchanged* if the function executes successfully.
89+
90+
.. code-block:: c
91+
:caption: If the pointer is set, the function encountered an error.
92+
:emphasize-lines: 5, 9
93+
94+
// ... snip ...
95+
struct error* error = NULL;
96+
97+
bin_op(stack, op, &error);
98+
if (error != NULL)
99+
{
100+
// Let the user know the operation could not be executed.
101+
// ... snip ...
102+
error_del(&error);
103+
}
104+
105+
If an error can be safely ignored, you **may** call the function with a null pointer.
106+
It is equivalent to a try-catch with an empty catch block in languages that have exceptions.
107+
This does not mean the error will not be handled, it means the error can be handled by doing nothing.
108+
109+
.. code-block:: c
110+
:caption: A null pointer signifies that the error is handled by doing nothing.
111+
:emphasize-lines: 15
112+
113+
bool
114+
update(int id, struct error** error)
115+
{
116+
if (!exists(id))
117+
{
118+
set_error(error, LNOENT, "no entity with the given id: %d", id);
119+
return false;
120+
}
121+
// ... snip ...
122+
}
123+
124+
void
125+
update_if_exists(int id)
126+
{
127+
update(id, NULL);
128+
}
129+
130+
When nesting functions that can report errors it is important to not reuse the error pointer provided to the parent function, as it may be null.
131+
Instead you **should** create a temporary error pointer.
132+
133+
.. code-block:: c
134+
:caption: Call the child function with a temporary error pointer.
135+
:emphasize-lines: 5, 10
136+
137+
void
138+
parent(struct error** error)
139+
{
140+
struct error* tmp_error = NULL;
141+
child(&tmp_error);
142+
if (tmp_error != NULL)
143+
{
144+
// Handle error from the child function.
145+
// ... snip ...
146+
// Call error_del or error_propagate.
147+
}
148+
}
149+
150+
Propagating errors
151+
__________________
152+
153+
When a caller cannot handle an error from a nested function call, you **may** propagate the error upwards with :code:`error_propagate`.
154+
It is equivalent to calling a function that throws exceptions outside of a try-catch block or catching and re-throwing in languages that have exceptions.
155+
When propagating an error you don't need to worry about ownership, :code:`error_propagate` is smart enough to free the error in cases where the caller chose to ignore it or transfer ownership otherwise.
156+
157+
.. code-block:: c
158+
:caption: This error cannot be handled here but may be handled by the caller of :code:`parse`, propagate it.
159+
:emphasize-lines: 12
160+
161+
bool
162+
parse(const char* src, struct slice* tokens, struct error** error)
163+
{
164+
struct error* tmp_error = NULL;
165+
166+
// ... snip ...
167+
parse_ident(src, ptr, tokens, &tmp_error);
168+
if (tmp_error != NULL)
169+
{
170+
// We need an identifier but couldn't parse one,
171+
// there's nothing we can do to fix that.
172+
error_propagate(&tmp_error, error);
173+
return false;
174+
}
175+
}
176+
177+
178+
If all you need is to propagate an error and return, :code:`error_bubble` can help avoid repeating the if statement from the previous example:
179+
180+
.. code-block:: c
181+
:caption: Propagate and return false immediately on error.
182+
:emphasize-lines: 8
183+
184+
bool
185+
parse(const char* src, struct slice* tokens, struct error** error)
186+
{
187+
struct error* tmp_error = NULL;
188+
189+
// ... snip ...
190+
parse_ident(src, ptr, tokens, &tmp_error);
191+
error_bubble(&tmp_error, error, false);
192+
}
193+
194+
195+
Performance
196+
___________
197+
198+
Reporting errors requires a memory allocation and string formatting.
199+
For more performant reports you **may** avoid the string formatting with :code:`error_set_literal`, or fallback to integer error codes.
200+
201+
TODO
202+
----
203+
204+
.. todo::
205+
206+
- Safe strings.
207+
208+
.. todo::
209+
210+
- Warnings and assertions when error functions are used incorrectly.
211+
212+
API
213+
---
214+
.. doxygenfile:: error.h
215+
:sections: briefdescription detaileddescription
216+
217+
Enums
218+
_____
219+
.. doxygenenum:: error_code
220+
221+
Handle
222+
______
223+
.. doxygenstruct:: error
224+
:members:
225+
226+
Functions
227+
_________
228+
.. doxygenfunction:: error_set
229+
.. doxygenfunction:: error_set_literal
230+
.. doxygenfunction:: error_del
231+
.. doxygenfunction:: error_propagate
232+
233+
Macros
234+
______
235+
.. doxygendefine:: panic
236+
.. doxygendefine:: error_bubble
237+
.. doxygendefine:: error_bubble_void

docs/lib/overview/index.rst

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,5 @@ Overview
22
========
33

44
.. toctree::
5-
:glob:
6-
7-
*
5+
errors
6+
comparators

0 commit comments

Comments
 (0)