
Microvium has try-catch!
TL;DR: Microvium now supports exception handling, with just 4 bytes of overhead per active try block. This post covers some of the details of the design, for those who are interested.
Note: if you’re new to try-catch, see the MDN article on the subject. Note however that Microvium does not yet support finally
, but in my experience finally
is a less-used feature of JS.
Why try-catch?
One of my objectives with Microvium is to allow users to write JS code that is stylistically similar to the kind of code they would write for a larger engine like V8/Node.js. I want it to be easy to write JS libraries that run on both Microvium and Node, and the public interface of these libraries especially should not be contorted to fit the Microvium feature subset.
A major thing that’s been missing from this vision is the ability to use exceptions to pass errors. A library that can’t use exceptions to pass errors must use some other technique such as error codes, which are not typical JavaScript style and would clutter up the API of such a library.
A look at alternatives
Microvium is implemented in C. How does one implement exceptions in a language like C? In the journey of finding the best way to do exceptions, I thought of a number of different ideas.
The most obvious one to come to mind is something like setjmp/longjmp. This is a common way to implement exception-like behavior in a language like C, since it’s the only way to jump from one C function into another that’s earlier on the stack. At face value, this sounds sensible since both the engine and host are running on C.
Looking at other embedded JavaScript engines, mJS and Elk don’t support exceptions at all, but Moddable’s XS uses this setjmp
/longjmp
approach. At each try
, XS mallocs a jump structure which is populated with the state of current virtual registers and physical registers, as well as a pointer to the catch
code that would handle the exception. Upon encountering a throw
, all the virtual and physical registers are restored to their saved state, which implicitly unwinds the stack, and then control is moved to the referenced catch block.
In my measurements1, one of these jump structures in XS is 120-140 bytes on an embedded ARM architecture and 256-312 bytes on my x64 machine2. Even the jmp_buf
on its own is 92 bytes on Cortex M0 (the architecture to which I’ve targetted all my size tests in Microvium). That’s quite heavy! Not to mention the processing overhead of doing a malloc
and populating this structure (each time a try
block is entered).
How it works in Microvium
After thinking about it for some time, the following is the solution I settled on. Consider the following code, where we have 2 variables on the stack, and 2 try-catch blocks:

If we were to throw an exception on line 5, what needs to happen?
- We need to pop the variable
y
off the stack (but notx
), since we’re exiting its containing scope. This is called unwinding the stack. - We need to pass control to the
e1
catch clause (line 7) - We need to now record that the
catch(e2)
on line 11 is the next outer catch block, in case there is anotherthrow
Microvium does this by keeping a linked list of try
blocks on the stack. Each try
block in the linked list is 2 words (4 bytes): a pointer to the bytecode address of the corresponding catch
bytecode, plus a pointer to the next outer try
block in the linked list. For the previous example, there will be 2 try
blocks on the stack when we’re at line 5, as shown in the following diagram:

Each of the smaller rectangles here represents a 16-bit word (aka slot). The arrows here represent pointers. The red blocks (with an extra border) each represent an active try
block consisting of 2 words (catch
and nextTry
). They form a linked list because each has a pointer to the next outer try
block.
The topTry
register points to the inner-most active try
block — the head of the linked list. Each time the program enters a try
statement, Microvium will push another try
block to the stack and record its stack address in the topTry
register.
The topTry
register serves 2 purposes simultaneously. For one, it points to the head of the linked list, which allows us to find the associated catch
block when an exception is thrown. But it also points to exactly the stack level we need to unwind
to when an exception is thrown. This is similarly true for each nextTry
pointer.
When an exception is thrown, Microvium will:
- Take the current
topTry
and unwind the stack to the level it points to. - Transfer control (the program counter) to the bytecode address of the catch block which is now sitting at the top of the stack (i.e. pop the program counter off the stack).
- Save the
nextTry
pointer as the newtopTry
(i.e. it pops thetopTry
register off the stack).
Et voila! We’ve implemented try-catch
in Microvium with just 4 bytes of overhead per try
.
This also works when throw
ing from one function to a catch
in a caller function. The only difference is that the unwind
process in step 1 then needs to restore the registers of the caller (e.g. argument count, closure state, return address, etc). This information is already on the stack since it’s also used by the return
instruction — we don’t need to save it separately during a try
. This works for any level of nested calls if we just unwind repeatedly until reaching the frame containing the catch target.
Conclusion
I’m pretty satisfied with this design.
- Only one new virtual register (
topTry
) - Only 4 bytes of RAM per active
try
3 - No heap overhead
- Conceptually simple
- Only makes the engine 156 bytes larger in ROM
The static analysis to implement this is quite hairy — it’s complicated by the interaction between try
…catch
and return
, break
, and closures, among other things. For those interested, see the test cases for examples of the kinds of scenarios that try-catch needs to deal with.
On a meta-level, this is a good reminder of how the first idea that comes to mind is often not the best one, and the benefit of thinking through a problem before jumping right in. I’ve been brainstorming ideas about how exceptions might work in Microvium since July 2020 — almost 2 years ago — and I kept going back to the ideas to revise and reconsider, going back and forth between different options. Sometimes, mulling over an idea over a long period of time can help you get to a much better solution in the end.
Another lesson is that simplicity is deceptively time-consuming. Looking only at this apparently-simple final design, you might not guess at the amount of work required to get there. This is a general tragedy of software engineering: it often takes more time to get to a simpler solution, which gives the appearance of achieving less while taking longer to do so.
One Reply to “Microvium has try-catch!”
Awesome! Can’t wait to play with it!