Welcome to the Tsonnet series!
If you're just joining us, you can check out how it all started in the first post of the series.
In the previous post, we replaced exception handling with monadic error handling using Result:
Now that we've replaced exceptions with the result type, let's improve our error handling to deal with malformed strings.
Handling unterminated string
Here's a Jsonnet sample containing an unterminated string that we'll use to test Tsonnet:
Feeding it to Jsonnet get us the following output:
We want a similar output and the same exit error code.
The lexer already has code to catch malformed strings, but I forgot to add a test case for it before.
Let's add a new cram test to cover it:
The error message differs slightly from Jsonnet's output, but that's acceptable -- we don't need an exact match. While we're missing the file location, line number, and column information in the error trace, we'll implement those in a future update.
The exit code can be expressed in a cram test by putting the exit code between square brackets after the returned error message. This is the pattern used by dune to assert the exit code.
In order to catch the exceptions raised by the lexer, we need to expose it to the other modules. In OCaml we do that by writing the public signatures of functions and definitions in a .mli
file -- similar to C header files, but OCaml:
We could potentially replace the exceptions here by result types too. I want to explore this way of designing the lexer and parser, but I will refrain myself of doing it right now to avoid losing the focus on the purpose of this change -- I'll come back to it later.
And now we can catch the SyntaxError
exception in the parse
function in the Tsonnet module:
I took the liberty of using >>=
as an alias for chaining function calls returning Result. After you get used to it, this monadic style feels quite natural — similar to the pipe operator in bash scripts! The bind
function unwraps the value from a Result and passes it to the next function, but only continues the chain if there's no failure.
Turns out Tsonnet is neither outputting a clean error message nor the correct exit code:
This is due to failwith
— it raises an exception and halts the execution with exit code 2, which is not semantically correct. We need to explicitly print the message to stderr
and exit with code 1:
And now it does:
Conclusion
With these changes, we've improved our error handling to properly report malformed strings, direct errors to stderr, and return semantically correct exit codes. These small but important details make Tsonnet more robust and user-friendly.