I've decided to continue working on my template library (horrorshow) and have gotten to the point where I need to start thinking about error handling.
The Problem
I have two error conditions:
- Write errors: errors when writing to the template output. I'd like to allow users to render directly into an IO writer by calling something like
template.render_into_io(&mut some_io_writer)
(wheresome_io_writer: std::io::Write
). However, writing tosome_io_writer
can obviously fail. - Render errors: These are user generated errors that prevent rendering from continuing. For example, someone might write the following:
html! {
p {
// This is the current syntax for inline code (`tmpl` is the current template).
|tmpl| if !current_user.is_admin() {
Err("Current user not an admin");
} else {
tmpl << "Welcome Admin!"; //Note: this returns a Result<(), io::Error>
};
}
Designs
Results Everywhere
This is the design used above. Every inline closure returns a result and every write to the template returns a result. If a write fails, it's the user's responsibility to bubble the error. Unfortunately, this design has some drawbacks:
First, non IO errors (Render errors) need to be stored as Box<Error>
. Otherwise, composing templates becomes painful and the types become very messy.
Second, everything needs to return a result. This means you can't just write:
html! {
ul {
|tmpl| for i in 0..10 {
try!(tmpl << html! { li : "Hi!" })
}
}
}
Everything needs a dangling Ok(())
:
html! {
ul {
|tmpl| {
for i in 0..10 {
try!(tmpl << html! { li : "Hi!" })
}
Ok(())
}
}
}
Errors On The Side
Alternatively, I could write stash errors. That is, if writing to the underlying Write fails, I stash the error and continue (but drop any future writes on the floor). When rendering is done, I check to see if I have a stashed error and return it. This means that template code doesn't have to care write errors. This shouldn't cause any problems because template rendering can fail at any time anyways.
The above code would become:
html! {
ul {
|tmpl| for i in 0..10 {
tmpl << html! { li : "Hi!" }
}
}
}
Now how to handle user errors:
- I could provide a
set_error
method on the template (tmpl.set_error(e)
). The first error set would win. - Only allow
panic!()
. I could tell users to check all reasonable error conditions ahead of time. Bailing out due to a non Write error is a bad idea anyways because one could be half-way trough sending data to a client (??). - Allow two type signatures on closures: One that returns a
Result<(), Box<Error>>
and one that returns()
.
Option 3 looks nice but has a nasty edge case. I'd like to stop processing the template as soon as we hit a user error but that won't happen when rendering sub-templates:
html! {
|tmpl| {
tmpl << html! {
return Err("fail");
};
// Code down here will continue to run...
}
}
I could make a distinction between templates that can fail due to user defined errors and templates that don't fail and force the user to handle Render errors (user defined errors) but not Write errors.
The current version of horrorshow (on github) implements "Errors On The Side" and doesn't allow custom user errors (the only way for a user to abort is to panic).