diff --git a/py/bc.h b/py/bc.h
index 814201160b4f898eed8f19c8cdf60809613b504b..aac35954d7add7eadf20b4d66fda97c951181eab 100644
--- a/py/bc.h
+++ b/py/bc.h
@@ -1,3 +1,9 @@
-mp_obj_t mp_execute_byte_code(const byte *code, const mp_obj_t *args, uint n_args, const mp_obj_t *args2, uint n_args2, uint n_state);
-bool mp_execute_byte_code_2(const byte *code_info, const byte **ip_in_out, mp_obj_t *fastn, mp_obj_t **sp_in_out);
+typedef enum {
+    MP_VM_RETURN_NORMAL,
+    MP_VM_RETURN_YIELD,
+    MP_VM_RETURN_EXCEPTION,
+} mp_vm_return_kind_t;
+
+mp_vm_return_kind_t mp_execute_byte_code(const byte *code, const mp_obj_t *args, uint n_args, const mp_obj_t *args2, uint n_args2, uint n_state, mp_obj_t *ret);
+mp_vm_return_kind_t mp_execute_byte_code_2(const byte *code_info, const byte **ip_in_out, mp_obj_t *fastn, mp_obj_t **sp_in_out);
 void mp_byte_code_print(const byte *code, int len);
diff --git a/py/objexcept.c b/py/objexcept.c
index dbe702fa9e72f96c6a18de4f9acbd905233446bc..65ec533164fc1ba785f2b8fab1ea3e75e8a8970f 100644
--- a/py/objexcept.c
+++ b/py/objexcept.c
@@ -27,6 +27,7 @@ STATIC void mp_obj_exception_print(void (*print)(void *env, const char *fmt, ...
         print(env, "%s: %s", qstr_str(o->base.type->name), vstr_str(o->msg));
     } else {
         // Yes, that's how CPython has it
+        // TODO now that exceptions are classes and instances, I think this needs to be changed to match CPython
         if (kind == PRINT_REPR) {
             print(env, "%s", qstr_str(o->base.type->name));
         }
@@ -162,7 +163,7 @@ void mp_obj_exception_add_traceback(mp_obj_t self_in, qstr file, machine_uint_t
 
     // for traceback, we are just using the list object for convenience, it's not really a list of Python objects
     if (self->traceback == MP_OBJ_NULL) {
-        self->traceback = mp_obj_new_list(3, NULL);
+        self->traceback = mp_obj_new_list(0, NULL);
     }
     mp_obj_list_append(self->traceback, (mp_obj_t)(machine_uint_t)file);
     mp_obj_list_append(self->traceback, (mp_obj_t)(machine_uint_t)line);
diff --git a/py/objfun.c b/py/objfun.c
index 433da039bb2614e9e8173428fa4c7f4601225fa6..0c7ccf798479bc372a13cda6df339a83f650adf4 100644
--- a/py/objfun.c
+++ b/py/objfun.c
@@ -152,10 +152,15 @@ STATIC mp_obj_t fun_bc_call(mp_obj_t self_in, uint n_args, uint n_kw, const mp_o
     uint use_def_args = self->n_args - n_args;
     mp_map_t *old_globals = rt_globals_get();
     rt_globals_set(self->globals);
-    mp_obj_t result = mp_execute_byte_code(self->bytecode, args, n_args, self->def_args + self->n_def_args - use_def_args, use_def_args, self->n_state);
+    mp_obj_t result;
+    mp_vm_return_kind_t vm_return_kind = mp_execute_byte_code(self->bytecode, args, n_args, self->def_args + self->n_def_args - use_def_args, use_def_args, self->n_state, &result);
     rt_globals_set(old_globals);
 
-    return result;
+    if (vm_return_kind == MP_VM_RETURN_NORMAL) {
+        return result;
+    } else { // MP_VM_RETURN_EXCEPTION
+        nlr_jump(result);
+    }
 }
 
 const mp_obj_type_t fun_bc_type = {
diff --git a/py/objgenerator.c b/py/objgenerator.c
index 226b902dafaf85b9f091692325a36a62beb312c3..6a03af856bc5159a80996a3cb874283ae7c9c629 100644
--- a/py/objgenerator.c
+++ b/py/objgenerator.c
@@ -82,22 +82,30 @@ STATIC mp_obj_t gen_next_send(mp_obj_t self_in, mp_obj_t send_value) {
     } else {
         *self->sp = send_value;
     }
-    bool yield = mp_execute_byte_code_2(self->code_info, &self->ip, &self->state[self->n_state - 1], &self->sp);
-    if (yield) {
-        return *self->sp;
-    } else {
-        // Explicitly mark generator as completed. If we don't do this,
-        // subsequent next() may re-execute statements after last yield
-        // again and again, leading to side effects.
-        // TODO: check how return with value behaves under such conditions
-        // in CPython.
-        self->ip = 0;
-        if (*self->sp == mp_const_none) {
-            return mp_const_stop_iteration;
-        } else {
-            // TODO return StopIteration with value *self->sp
-            return mp_const_stop_iteration;
-        }
+    mp_vm_return_kind_t vm_return_kind = mp_execute_byte_code_2(self->code_info, &self->ip, &self->state[self->n_state - 1], &self->sp);
+    switch (vm_return_kind) {
+        case MP_VM_RETURN_NORMAL:
+            // Explicitly mark generator as completed. If we don't do this,
+            // subsequent next() may re-execute statements after last yield
+            // again and again, leading to side effects.
+            // TODO: check how return with value behaves under such conditions
+            // in CPython.
+            self->ip = 0;
+            if (*self->sp == mp_const_none) {
+                return mp_const_stop_iteration;
+            } else {
+                // TODO return StopIteration with value *self->sp
+                return mp_const_stop_iteration;
+            }
+
+        case MP_VM_RETURN_YIELD:
+            return *self->sp;
+
+        case MP_VM_RETURN_EXCEPTION:
+        default:
+            // TODO
+            assert(0);
+            return mp_const_none;
     }
 }
 
diff --git a/py/vm.c b/py/vm.c
index 461fecbda87e8f142286aaff995c8bb4a0ce9da8..573167b57d51ee6bf6afc8580d437a977af61df0 100644
--- a/py/vm.c
+++ b/py/vm.c
@@ -47,7 +47,7 @@ typedef enum {
 #define TOP() (*sp)
 #define SET_TOP(val) *sp = (val)
 
-mp_obj_t mp_execute_byte_code(const byte *code, const mp_obj_t *args, uint n_args, const mp_obj_t *args2, uint n_args2, uint n_state) {
+mp_vm_return_kind_t mp_execute_byte_code(const byte *code, const mp_obj_t *args, uint n_args, const mp_obj_t *args2, uint n_args2, uint n_state, mp_obj_t *ret) {
     // allocate state for locals and stack
     mp_obj_t temp_state[10];
     mp_obj_t *state = &temp_state[0];
@@ -83,20 +83,30 @@ mp_obj_t mp_execute_byte_code(const byte *code, const mp_obj_t *args, uint n_arg
     }
 
     // execute the byte code
-    if (mp_execute_byte_code_2(code, &ip, &state[n_state - 1], &sp)) {
-        // it shouldn't yield
-        assert(0);
+    mp_vm_return_kind_t vm_return_kind = mp_execute_byte_code_2(code, &ip, &state[n_state - 1], &sp);
+
+    switch (vm_return_kind) {
+        case MP_VM_RETURN_NORMAL:
+            *ret = *sp;
+            return MP_VM_RETURN_NORMAL;
+        case MP_VM_RETURN_EXCEPTION:
+            *ret = state[n_state - 1];
+            return MP_VM_RETURN_EXCEPTION;
+        case MP_VM_RETURN_YIELD: // byte-code shouldn't yield
+        default:
+            assert(0);
+            *ret = mp_const_none;
+            return MP_VM_RETURN_NORMAL;
     }
-
-    // TODO check fails if, eg, return from within for loop
-    //assert(sp == &state[17]);
-    return *sp;
 }
 
 // fastn has items in reverse order (fastn[0] is local[0], fastn[-1] is local[1], etc)
 // sp points to bottom of stack which grows up
-// returns true if bytecode yielded
-bool mp_execute_byte_code_2(const byte *code_info, const byte **ip_in_out, mp_obj_t *fastn, mp_obj_t **sp_in_out) {
+// returns:
+//  MP_VM_RETURN_NORMAL, sp valid, return value in *sp
+//  MP_VM_RETURN_YIELD, ip, sp valid, yielded value in *sp
+//  MP_VM_RETURN_EXCEPTION, exception in fastn[0]
+mp_vm_return_kind_t mp_execute_byte_code_2(const byte *code_info, const byte **ip_in_out, mp_obj_t *fastn, mp_obj_t **sp_in_out) {
     // careful: be sure to declare volatile any variables read in the exception handler (written is ok, I think)
 
     const byte *ip = *ip_in_out;
@@ -569,7 +579,7 @@ unwind_return:
                         nlr_pop();
                         *sp_in_out = sp;
                         assert(exc_sp == &exc_stack[0] - 1);
-                        return false;
+                        return MP_VM_RETURN_NORMAL;
 
                     case MP_BC_RAISE_VARARGS:
                         unum = *ip++;
@@ -581,7 +591,7 @@ unwind_return:
                         nlr_pop();
                         *ip_in_out = ip;
                         *sp_in_out = sp;
-                        return true;
+                        return MP_VM_RETURN_YIELD;
 
                     case MP_BC_IMPORT_NAME:
                         DECODE_QSTR;
@@ -603,7 +613,7 @@ unwind_return:
                         printf("code %p, byte code 0x%02x not implemented\n", ip, op);
                         assert(0);
                         nlr_pop();
-                        return false;
+                        return MP_VM_RETURN_NORMAL;
                 }
             }
 
@@ -653,9 +663,10 @@ unwind_return:
                 PUSH(nlr.ret_val); // TODO should be type(nlr.ret_val), I think...
 
             } else {
-                // re-raise exception to higher level
-                // TODO what to do if this is a generator??
-                nlr_jump(nlr.ret_val);
+                // propagate exception to higher level
+                // TODO what to do about ip and sp? they don't really make sense at this point
+                fastn[0] = nlr.ret_val; // must put exception here because sp is invalid
+                return MP_VM_RETURN_EXCEPTION;
             }
         }
     }
diff --git a/tests/basics/try-module.py b/tests/basics/try-module.py
index e62df8487dd97917635a5945e134f5ec987ad1d3..03a9db15b5702657592cb69531b885144a55914a 100644
--- a/tests/basics/try-module.py
+++ b/tests/basics/try-module.py
@@ -3,7 +3,7 @@
 import import1b
 
 def func1():
-    return
+    print('func1')
 
 def func2():
     try: