Error Handling Design in Horrorshow

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:

  1. 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) (where some_io_writer: std::io::Write). However, writing to some_io_writer can obviously fail.
  2. 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:

  1. I could provide a set_error method on the template (tmpl.set_error(e)). The first error set would win.
  2. 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 (??).
  3. 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).

I've only spent a few minutes reading your post and your crate docs, so apologies if I've missed something!

Roughly in order of personal preference:

My first thought is: can you implement a deeply embedded DSL instead of a shallow embedding? That is, invent an intermediate representation for your templating language. That should give you the flexibility to deal with errors any way you like. I'm not sure how feasible this is given your design constraints (including whether it's possible to do it with just macro_rules!). I think this would help solve the sub-template problem at least.

My second thought is: I definitely agree that sticking try! everywhere and inserting Ok(()) in certain places is really unattractive. It might even be a non-starter IMO. Errors on the side seems like a good path to take if you can pull it off. Could you delay the handling of errors to the call to render somehow?

My third thought is: while atrociously unidiomatic, you can actually catch a panic, which will give you an Any on error. Then you can downcast it.

Thank you for the feedback!

I'd have to parse rust (or some equivalent language) so I'd rather not. Anyways, I really want to be able to embed normal rust closures that don't do anything unexpected. This way, I keep the DSL small and let users write logic in an actual language (i.e. rust).

This is what I currently do for write errors. Example:

let tmpl = html! { /* */ }; // No errors (actually, almost nothing happens here).
let mut string = String::new();
try!(tmpl.write_to_string(&mut string)); // This is where I render the template and return errors if any.

The problem is that edge case I mentioned. Desugared and expanded, the example code would be actually be:

let t = html! {
    |tmpl| {
        (html! {
            return Err("fail");
        }).render_once(tmpl); // Render the sub-template into this template.
        // Code down here will continue to run...
        return Err("fail2");
    }
};
let mut s = String::new();
assert_eq!(t.write_to_string(&mut s), Err("fail"));

When rendering into tmpl, the sub-template would encounter the Err("fail") and stash it inside of tmpl. Unfortunately, this wouldn't cause template rendering to abort immediately. Additionally, the second error would just be thrown away unless I return a collection of errors.

Actually, does the following sounds reasonable?

  1. Rendering never aborts early.
  2. Closures/sub-templates embedded in a template can return as many errors as they want.
  3. All returned errors produced will be collected into an error set and returned from the top-level render call (i.e. write_to_string and friends).

If the user wants to panic, that's up to them. It's actually reasonable in certain circumstances but I'd rather not build panicking behavior into a library.