diff --git a/maud/src/lib.rs b/maud/src/lib.rs
index f884d71..b08d53a 100644
--- a/maud/src/lib.rs
+++ b/maud/src/lib.rs
@@ -5,9 +5,6 @@
 //!
 //! [book]: http://lfairy.gitbooks.io/maud/content/
 
-use std::fmt;
-use std::io;
-
 /// Escapes an HTML value.
 pub fn escape(s: &str) -> String {
     use std::fmt::Write;
@@ -16,68 +13,11 @@ pub fn escape(s: &str) -> String {
     buf
 }
 
-/// A block of HTML markup, as returned by the `html!` macro.
-///
-/// Use `.to_string()` to convert it to a `String`, or `.render()` to
-/// write it directly to a handle.
-pub struct Markup<F> {
-    callback: F,
-}
-
-impl<F> Markup<F> where F: Fn(&mut fmt::Write) -> fmt::Result {
-    /// Renders the markup to a `std::io::Write`.
-    pub fn render<W: io::Write>(&self, w: &mut W) -> io::Result<()> {
-        struct Adaptor<'a, W: ?Sized + 'a> {
-            inner: &'a mut W,
-            error: io::Result<()>,
-        }
-
-        impl<'a, W: ?Sized + io::Write> fmt::Write for Adaptor<'a, W> {
-            fn write_str(&mut self, s: &str) -> fmt::Result {
-                match self.inner.write_all(s.as_bytes()) {
-                    Ok(()) => Ok(()),
-                    Err(e) => {
-                        self.error = Err(e);
-                        Err(fmt::Error)
-                    },
-                }
-            }
-        }
-
-        let mut output = Adaptor { inner: w, error: Ok(()) };
-        match self.render_fmt(&mut output) {
-            Ok(()) => Ok(()),
-            Err(_) => output.error,
-        }
-    }
-
-    /// Renders the markup to a `std::fmt::Write`.
-    pub fn render_fmt(&self, w: &mut fmt::Write) -> fmt::Result {
-        (self.callback)(w)
-    }
-}
-
-impl<F> ToString for Markup<F> where F: Fn(&mut fmt::Write) -> fmt::Result {
-    fn to_string(&self) -> String {
-        let mut buf = String::new();
-        self.render_fmt(&mut buf).unwrap();
-        buf
-    }
-}
-
 /// Internal functions used by the `maud_macros` package. You should
 /// never need to call these directly.
 #[doc(hidden)]
 pub mod rt {
     use std::fmt;
-    use super::Markup;
-
-    #[inline]
-    pub fn make_markup<F>(f: F) -> Markup<F> where
-        F: Fn(&mut fmt::Write) -> fmt::Result
-    {
-        Markup { callback: f }
-    }
 
     pub struct Escaper<'a, 'b: 'a> {
         pub inner: &'a mut (fmt::Write + 'b),
diff --git a/maud_macros/src/lib.rs b/maud_macros/src/lib.rs
index 2f27d9b..87695fd 100644
--- a/maud_macros/src/lib.rs
+++ b/maud_macros/src/lib.rs
@@ -36,6 +36,6 @@ fn expand_html_debug<'cx>(cx: &'cx mut ExtCtxt, sp: Span, args: &[TokenTree]) ->
 
 #[plugin_registrar]
 pub fn plugin_registrar(reg: &mut Registry) {
-    reg.register_macro("html", expand_html);
-    reg.register_macro("html_debug", expand_html_debug);
+    reg.register_macro("write_html", expand_html);
+    reg.register_macro("write_html_debug", expand_html_debug);
 }
diff --git a/maud_macros/src/parse.rs b/maud_macros/src/parse.rs
index 9071018..35b352c 100644
--- a/maud_macros/src/parse.rs
+++ b/maud_macros/src/parse.rs
@@ -43,16 +43,30 @@ macro_rules! ident {
 }
 
 pub fn parse(cx: &ExtCtxt, input: &[TokenTree], sp: Span) -> P<Expr> {
+    let (write, input) = split_comma(cx, input, sp);
     let mut parser = Parser {
         in_attr: false,
         input: input,
         span: sp,
-        render: Renderer::new(cx),
+        render: Renderer::new(cx, write.to_vec()),
     };
     parser.markups();
     parser.into_render().into_expr()
 }
 
+fn split_comma<'a>(cx: &ExtCtxt, input: &'a [TokenTree], sp: Span) -> (&'a [TokenTree], &'a [TokenTree]) {
+    fn is_comma(t: &TokenTree) -> bool {
+        match *t {
+            TtToken(_, token::Comma) => true,
+            _ => false,
+        }
+    }
+    match input.iter().position(is_comma) {
+        Some(i) => (&input[..i], &input[1+i..]),
+        None => cx.span_fatal(sp, "expected two arguments to write_html!()"),
+    }
+}
+
 struct Parser<'cx, 'i> {
     in_attr: bool,
     input: &'i [TokenTree],
diff --git a/maud_macros/src/render.rs b/maud_macros/src/render.rs
index 5294ed9..e57f8de 100644
--- a/maud_macros/src/render.rs
+++ b/maud_macros/src/render.rs
@@ -1,6 +1,6 @@
-use syntax::ast::{Expr, Ident, Pat, Stmt, TokenTree};
+use syntax::ast::{Expr, Ident, Pat, Stmt, TokenTree, TtToken};
+use syntax::codemap::DUMMY_SP;
 use syntax::ext::base::ExtCtxt;
-use syntax::ext::build::AstBuilder;
 use syntax::parse::token;
 use syntax::ptr::P;
 
@@ -15,17 +15,25 @@ pub enum Escape {
 pub struct Renderer<'cx> {
     pub cx: &'cx ExtCtxt<'cx>,
     w: Ident,
+    r: Ident,
+    loop_label: Vec<TokenTree>,
     stmts: Vec<P<Stmt>>,
     tail: String,
 }
 
 impl<'cx> Renderer<'cx> {
     /// Creates a new `Renderer` using the given extension context.
-    pub fn new(cx: &'cx ExtCtxt<'cx>) -> Renderer<'cx> {
+    pub fn new(cx: &'cx ExtCtxt<'cx>, writer_expr: Vec<TokenTree>) -> Renderer<'cx> {
+        let w = token::gensym_ident("__maud_writer");
+        let r = token::gensym_ident("__maud_result");
+        let loop_label = token::gensym_ident("__maud_loop_label");
+        let writer_stmt = quote_stmt!(cx, let $w = &mut $writer_expr).unwrap();
         Renderer {
             cx: cx,
-            w: Ident::new(token::intern("w")),
-            stmts: Vec::new(),
+            w: w,
+            r: r,
+            loop_label: vec![TtToken(DUMMY_SP, token::Lifetime(loop_label))],
+            stmts: vec![writer_stmt],
             tail: String::new(),
         }
     }
@@ -35,6 +43,8 @@ impl<'cx> Renderer<'cx> {
         Renderer {
             cx: self.cx,
             w: self.w,
+            r: self.r,
+            loop_label: self.loop_label.clone(),
             stmts: Vec::new(),
             tail: String::new(),
         }
@@ -48,7 +58,7 @@ impl<'cx> Renderer<'cx> {
                 let s = &*self.tail;
                 quote_expr!(self.cx, $w.write_str($s))
             };
-            let stmt = self.cx.stmt_expr(self.cx.expr_try(expr.span, expr));
+            let stmt = self.wrap_try(expr);
             self.stmts.push(stmt);
             self.tail.clear();
         }
@@ -56,13 +66,16 @@ impl<'cx> Renderer<'cx> {
 
     /// Reifies the `Renderer` into a block of markup.
     pub fn into_expr(mut self) -> P<Expr> {
-        let Renderer { cx, w, stmts, .. } = { self.flush(); self };
-        quote_expr!(cx,
-            ::maud::rt::make_markup(|$w: &mut ::std::fmt::Write| -> Result<(), ::std::fmt::Error> {
+        let Renderer { cx, r, loop_label, stmts, .. } = { self.flush(); self };
+        quote_expr!(cx, {
+            let mut $r = Ok(());
+            $loop_label: loop {
                 use ::std::fmt::Write;
                 $stmts
-                Ok(())
-            }))
+                break $loop_label;
+            }
+            $r
+        })
     }
 
     /// Reifies the `Renderer` into a raw list of statements.
@@ -82,6 +95,21 @@ impl<'cx> Renderer<'cx> {
         self.tail.push_str(s);
     }
 
+    /// Wraps an expression in a `try!` call.
+    fn wrap_try(&self, expr: P<Expr>) -> P<Stmt> {
+        let r = self.r;
+        let loop_label = &self.loop_label;
+        quote_stmt!(
+            self.cx,
+            match $expr {
+                Ok(()) => {},
+                Err(e) => {
+                    $r = Err(e);
+                    break $loop_label;
+                }
+            }).unwrap()
+    }
+
     /// Appends a literal string, with the specified escaping method.
     pub fn string(&mut self, s: &str, escape: Escape) {
         let escaped;
@@ -105,7 +133,7 @@ impl<'cx> Renderer<'cx> {
                         "{}",
                         $expr)),
         };
-        let stmt = self.cx.stmt_expr(self.cx.expr_try(expr.span, expr));
+        let stmt = self.wrap_try(expr);
         self.push(stmt);
     }
 
diff --git a/maud_macros/tests/tests.rs b/maud_macros/tests/tests.rs
index 3c7b1fa..91cf0ee 100644
--- a/maud_macros/tests/tests.rs
+++ b/maud_macros/tests/tests.rs
@@ -5,69 +5,75 @@ extern crate maud;
 
 #[test]
 fn literals() {
-    let s = html!("du\tcks" -23 3.14 '\n' "geese").to_string();
+    let mut s = String::new();
+    write_html!(s, "du\tcks" -23 3.14 '\n' "geese").unwrap();
     assert_eq!(s, "du\tcks-233.14\ngeese");
 }
 
 #[test]
 fn escaping() {
-    let s = html!("<flim&flam>").to_string();
+    let mut s = String::new();
+    write_html!(s, "<flim&flam>").unwrap();
     assert_eq!(s, "&lt;flim&amp;flam&gt;");
 }
 
 #[test]
 fn semicolons() {
-    let s = html! {
+    let mut s = String::new();
+    write_html!(s, {
         "one";
         "two";
         "three";
         ;;;;;;;;;;;;;;;;;;;;;;;;
         "four";
-    }.to_string();
+    }).unwrap();
     assert_eq!(s, "onetwothreefour");
 }
 
 #[test]
 fn blocks() {
-    let s = html! {
+    let mut s = String::new();
+    write_html!(s, {
         "hello"
         {
             " ducks" " geese"
         }
         " swans"
-    }.to_string();
+    }).unwrap();
     assert_eq!(s, "hello ducks geese swans");
 }
 
 mod elements {
     #[test]
     fn simple() {
-        let s = html! {
-            p { b { "pickle" } "barrel" i { "kumquat" } }
-        }.to_string();
+        let mut s = String::new();
+        write_html!(s, p { b { "pickle" } "barrel" i { "kumquat" } }).unwrap();
         assert_eq!(s, "<p><b>pickle</b>barrel<i>kumquat</i></p>");
     }
 
     #[test]
     fn nesting() {
-        let s = html!(html body div p sup "butts").to_string();
+        let mut s = String::new();
+        write_html!(s, html body div p sup "butts").unwrap();
         assert_eq!(s, "<html><body><div><p><sup>butts</sup></p></div></body></html>");
     }
 
     #[test]
     fn empty() {
-        let s = html!("pinkie" br/ "pie").to_string();
+        let mut s = String::new();
+        write_html!(s, "pinkie" br/ "pie").unwrap();
         assert_eq!(s, "pinkie<br>pie");
     }
 
     #[test]
     fn attributes() {
-        let s = html! {
+        let mut s = String::new();
+        write_html!(s, {
             link rel="stylesheet" href="styles.css"/
             section id="midriff" {
                 p class="hotpink" "Hello!"
             }
-        }.to_string();
+        }).unwrap();
         assert_eq!(s, concat!(
                 r#"<link rel="stylesheet" href="styles.css">"#,
                 r#"<section id="midriff"><p class="hotpink">Hello!</p></section>"#));
@@ -75,7 +81,8 @@ mod elements {
 
     #[test]
     fn empty_attributes() {
-        let s = html! { div readonly? input type="checkbox" checked? / }.to_string();
+        let mut s = String::new();
+        write_html!(s, div readonly? input type="checkbox" checked? /).unwrap();
         assert_eq!(s, r#"<div readonly><input type="checkbox" checked></div>"#);
     }
 }
@@ -83,19 +90,22 @@ mod elements {
 mod splices {
     #[test]
     fn literals() {
-        let s = html! { $"<pinkie>" }.to_string();
+        let mut s = String::new();
+        write_html!(s, $"<pinkie>").unwrap();
         assert_eq!(s, "&lt;pinkie&gt;");
     }
 
     #[test]
     fn raw_literals() {
-        let s = html! { $$"<pinkie>" }.to_string();
+        let mut s = String::new();
+        write_html!(s, $$"<pinkie>").unwrap();
         assert_eq!(s, "<pinkie>");
     }
 
     #[test]
     fn blocks() {
-        let s = html! {
+        let mut s = String::new();
+        write_html!(s, {
             ${
                 let mut result = 1i32;
                 for i in 2..11 {
@@ -103,19 +113,20 @@ mod splices {
                 }
                 result
             }
-        }.to_string();
+        }).unwrap();
         assert_eq!(s, "3628800");
     }
 
     #[test]
     fn attributes() {
         let rocks = true;
-        let s = html! {
+        let mut s = String::new();
+        write_html!(s, {
             input checked?=true /
             input checked?=false /
             input checked?=rocks /
             input checked?=(!rocks) /
-        }.to_string();
+        }).unwrap();
         assert_eq!(s, concat!(
                 r#"<input checked>"#,
                 r#"<input>"#,
@@ -127,14 +138,16 @@ mod splices {
 
     #[test]
     fn statics() {
-        let s = html! { $BEST_PONY }.to_string();
+        let mut s = String::new();
+        write_html!(s, $BEST_PONY).unwrap();
         assert_eq!(s, "Pinkie Pie");
     }
 
     #[test]
     fn closures() {
         let best_pony = "Pinkie Pie";
-        let s = html! { $best_pony }.to_string();
+        let mut s = String::new();
+        write_html!(s, $best_pony).unwrap();
         assert_eq!(s, "Pinkie Pie");
     }
 
@@ -159,37 +172,36 @@ mod splices {
             name: "Pinkie Pie",
             adorableness: 9,
         };
-        let s = html! {
+        let mut s = String::new();
+        write_html!(s, {
             "Name: " $pinkie.name ". Rating: " $pinkie.repugnance()
-        }.to_string();
+        }).unwrap();
         assert_eq!(s, "Name: Pinkie Pie. Rating: 1");
     }
 
     #[test]
     fn nested_macro_invocation() {
         let best_pony = "Pinkie Pie";
-        let s = html! { $(format!("{}", best_pony)) }.to_string();
+        let mut s = String::new();
+        write_html!(s, $(format!("{}", best_pony))).unwrap();
         assert_eq!(s, "Pinkie Pie");
     }
 }
 
-#[test]
-fn issue_1() {
-    let markup = html! { "Test" };
-    let _ = markup.to_string();
-}
-
 #[test]
 fn issue_13() {
     let owned = String::from("yay");
-    let _ = html! { $owned }.to_string();
+    let mut s = String::new();
+    write_html!(s, $owned).unwrap();
+    let _ = owned;
 }
 
 mod control {
     #[test]
     fn if_expr() {
         for (number, &name) in (1..4).zip(["one", "two", "three"].iter()) {
-            let s = html! {
+            let mut s = String::new();
+            write_html!(s, {
                 $if number == 1 {
                     "one"
                 } $else if number == 2 {
@@ -199,7 +211,7 @@ mod control {
                 } $else {
                     "oh noes"
                 }
-            }.to_string();
+            }).unwrap();
             assert_eq!(s, name);
         }
     }
@@ -207,13 +219,14 @@ mod control {
     #[test]
     fn if_let() {
         for &(input, output) in [(Some("yay"), "yay"), (None, "oh noes")].iter() {
-            let s = html! {
+            let mut s = String::new();
+            write_html!(s, {
                 $if let Some(value) = input {
                     $value
                 } $else {
                     "oh noes"
                 }
-            }.to_string();
+            }).unwrap();
             assert_eq!(s, output);
         }
     }
@@ -221,11 +234,12 @@ mod control {
     #[test]
     fn for_expr() {
         let ponies = ["Apple Bloom", "Scootaloo", "Sweetie Belle"];
-        let s = html! {
+        let mut s = String::new();
+        write_html!(s, {
             ul $for pony in &ponies {
                 li $pony
             }
-        }.to_string();
+        }).unwrap();
         assert_eq!(s, concat!(
                 "<ul>",
                 "<li>Apple Bloom</li>",