Error handling¶
There will be cases, when something goes wrong, and you want to bail out in an elegant way. If bailing out, and elegance can be used in the same sentence, that is. Depending on what kind of difficulty you are facing, you can indicate this to the user in different ways, and there seems to be a divide between programmers as to whether one should return an error code, or do something else.
But in the python world, the most common method is to raise some sort of exception, and let the user handle the problem. In the following snippet, we will see a couple of ways of going about exceptions. We implement a single function that raises an exception, no matter what. When developing user-friendly code, that is as vicious as you can get, I guess.
First, the code listing:
https://github.com/v923z/micropython-usermod/tree/master/snippets/sillyerrors/sillyerrors.c
#include "py/obj.h"
#include "py/builtin.h"
#include "py/runtime.h"
#include <stdlib.h>
STATIC mp_obj_t mean_function(mp_obj_t error_code) {
int e = mp_obj_get_int(error_code);
if(e == 0) {
mp_raise_msg(&mp_type_ZeroDivisionError, "thou shall not try to divide by 0 on a microcontroller!");
} else if(e == 1) {
mp_raise_msg(&mp_type_IndexError, "dude, that was a silly mistake!");
} else if(e == 2) {
mp_raise_TypeError("look, chap, you can't be serious!");
} else if(e == 3) {
mp_raise_OSError(e);
} else if(e == 4) {
char *buffer;
buffer = malloc(100);
sprintf(buffer, "you are really out of luck today: error code %d", e);
mp_raise_NotImplementedError(buffer);
} else {
mp_raise_ValueError("sorry, you've exhausted all your options");
}
return mp_const_false;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_1(mean_function_obj, mean_function);
STATIC const mp_rom_map_elem_t sillyerrors_module_globals_table[] = {
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_sillyerrors) },
{ MP_ROM_QSTR(MP_QSTR_mean), MP_ROM_PTR(&mean_function_obj) },
};
STATIC MP_DEFINE_CONST_DICT(sillyerrors_module_globals, sillyerrors_module_globals_table);
const mp_obj_module_t sillyerrors_user_cmodule = {
.base = { &mp_type_module },
.globals = (mp_obj_dict_t*)&sillyerrors_module_globals,
};
MP_REGISTER_MODULE(MP_QSTR_sillyerrors, sillyerrors_user_cmodule, MODULE_SILLYERRORS_ENABLED);
Now, not all exceptions are created equal. Some are more exceptional
than the others: ValueError
, TypeError
, OSError
, and
NotImplementedError
can be raised with the syntax
mp_raise_ValueError("wrong value");
which will, in addition to raising the exception at the C level (i.e., interrupting the execution of the code), also return a pretty traceback:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: wrong value
with the error message that we supplied to the mp_raise_ValueError
function. If you want to have a traceback message that is not a
compile-time constant, you could deal with the problem as in case 4 in
the function mean_function
. Such a message might be useful, if the
nature of the exception is somehow related to a quantity that is not
known at compile time, e.g., if you have a function that should not ever
run, if the up-time is shorter than some predefined value. Of course,
one can just say that the “microcontroller hasn’t run long enough yet”,
and this is a pretty good constant string, but perhaps we can give the
user a bit more information, if we can also indicate, how much time is
still missing.
Other exceptions can be raised as in the e == 1
case, with the
mp_raise_msg(&mp_type_IndexError, "dude, that was a silly mistake!")
function. Here one also has to specify the type of the exception, which
is always of the form mp_type_
. A complete list can be found in
obj.h
.
Incidentally, mp_raise_ValueError
, mp_raise_TypeError
, and
mp_raise_NotImplementedError
are nothing but a wrapper for
mp_raise_msg
, which in turn is a wrapper for nlr_raise
of
nlr.c/nlr.h
. The OSError
is somewhat curious in this respect,
because it is raised directly through nlr_raise
, and its argument is
not a string, but an integer error code. All these wrappers are defined
in runtime.c
, by the way.
In our ultimate mean function, we raised a lot of exceptions by now, but
we still have to return some value, because the function signature
stipulates that, and the compiler would be unsatisfied otherwise, even
though code execution will actually never reach the return statement.
Since we are in denial mode anyway, I cast my vote for a return value of
mp_const_false
. mp_const_none
was the other candidate, but ended
up as the runner-up.
I think, it is high time to compile our code.
https://github.com/v923z/micropython-usermod/tree/master/snippets/sillyerrors/micropython.mk
USERMODULES_DIR := $(USERMOD_DIR)
# Add all C files to SRC_USERMOD.
SRC_USERMOD += $(USERMODULES_DIR)/sillyerrors.c
CFLAGS_USERMOD += -I$(USERMODULES_DIR)
!make clean
!make USER_C_MODULES=../../../usermod/snippets CFLAGS_EXTRA=-DMODULE_SILLYERRORS_ENABLED=1 all
%%micropython
import sillyerrors
print(sillyerrors.mean(0))
Traceback (most recent call last):
File "/dev/shm/micropython.py", line 3, in <module>
ZeroDivisionError: thou shall not try to divide by 0 on a microcontroller!
%%micropython
import sillyerrors
print(sillyerrors.mean(1))
Traceback (most recent call last):
File "/dev/shm/micropython.py", line 3, in <module>
IndexError: dude, that was a silly mistake!
%%micropython
import sillyerrors
print(sillyerrors.mean(2))
Traceback (most recent call last):
File "/dev/shm/micropython.py", line 3, in <module>
TypeError: look, chap, you can't be serious!
%%micropython
import sillyerrors
print(sillyerrors.mean(3))
Traceback (most recent call last):
File "/dev/shm/micropython.py", line 3, in <module>
OSError: 3
%%micropython
import sillyerrors
print(sillyerrors.mean(4))
Traceback (most recent call last):
File "/dev/shm/micropython.py", line 3, in <module>
NotImplementedError: you are really out of luck today: error code 4
One can’t but wonder, why we had to invoke our mean
function in four
separate statements, and why we couldn’t execute everything in a nice
nifty package like
%%micropython
import sillyerrors
print(sillyerrors.mean(0))
print(sillyerrors.mean(1))
print(sillyerrors.mean(2))
print(sillyerrors.mean(3))
print(sillyerrors.mean(4))
Traceback (most recent call last):
File "/dev/shm/micropython.py", line 3, in <module>
ZeroDivisionError: you shall not try to divide by 0 on a microcontroller!
Well, we could have, but since we specifically raised an exception in the first statement, our code would never have gotten beyond
sillyerror.mean(0)
After all, this is what exceptions do: they interrupt the execution of the code.