|
| 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 |
0 commit comments