{"id":472,"date":"2026-07-17T13:04:21","date_gmt":"2026-07-17T13:04:21","guid":{"rendered":"https:\/\/abrarqasim.com\/blog\/rust-macros-2026-when-macro-rules-earns-its-place\/"},"modified":"2026-07-17T13:04:21","modified_gmt":"2026-07-17T13:04:21","slug":"rust-macros-2026-when-macro-rules-earns-its-place","status":"publish","type":"post","link":"https:\/\/abrarqasim.com\/blog\/rust-macros-2026-when-macro-rules-earns-its-place\/","title":{"rendered":"Rust Macros: When macro_rules! Earns Its Place (And When It Doesn&#8217;t)"},"content":{"rendered":"<p>I put off learning Rust macros for about a year. Every time I hit code that obviously wanted a macro, I&rsquo;d shrug, copy-paste the thing a few more times, and promise myself I&rsquo;d learn the macro system &ldquo;properly&rdquo; later. Later never showed up. What finally broke me was a <code>match<\/code> block I&rsquo;d rewritten so many times my git blame looked like it had a stutter.<\/p>\n<p>So I gave up a weekend and actually sat with them. Turns out I&rsquo;d been scared of the wrong half. <code>macro_rules!<\/code>, the declarative kind, is far friendlier than its reputation. The genuinely gnarly part, procedural macros, I reach for maybe once a project, if that. This is the guide I wish someone had handed me: what the two kinds are, when each one earns its keep, and the times I&rsquo;ve regretted writing a macro at all.<\/p>\n<h2 id=\"two-kinds-of-macros-and-why-the-split-matters\">Two kinds of macros, and why the split matters<\/h2>\n<p>Rust has two macro systems, and beginners (me, a year ago) tend to lump them together and get intimidated by the union of their worst parts.<\/p>\n<p>The first is <strong>declarative macros<\/strong>, written with <code>macro_rules!<\/code>. You pattern-match on syntax and expand into more syntax. Think of it as a very literal find-and-replace that understands Rust&rsquo;s grammar. Ninety percent of the macros you&rsquo;ll ever write are this kind, and they live right in your normal source files.<\/p>\n<p>The second is <strong>procedural macros<\/strong>: custom <code>derive<\/code>, attribute macros like <code>#[tokio::main]<\/code>, and function-like macros such as <code>sqlx::query!<\/code>. These run actual Rust code at compile time to generate code. They&rsquo;re strictly more powerful and strictly more annoying. They need their own crate, they pull in heavy dependencies, and they slow your builds down. The official <a href=\"https:\/\/doc.rust-lang.org\/book\/ch19-06-macros.html\" rel=\"nofollow noopener\" target=\"_blank\">macros chapter of the Rust book<\/a> lays out the taxonomy if you want the formal version.<\/p>\n<p>The practical takeaway: you write <code>macro_rules!<\/code>, you <em>consume<\/em> procedural macros other people wrote. Keep those in separate mental buckets and the fear mostly evaporates.<\/p>\n<h2 id=\"macro_rules-the-one-youll-actually-write\">macro_rules!: the one you&rsquo;ll actually write<\/h2>\n<p>Here&rsquo;s the code that finally sold me. I had a config-building block that looked like this, in a few different places:<\/p>\n<pre><code class=\"language-rust\">let mut config = HashMap::new();\nconfig.insert(&quot;host&quot;, &quot;localhost&quot;);\nconfig.insert(&quot;port&quot;, &quot;5432&quot;);\nconfig.insert(&quot;user&quot;, &quot;claude&quot;);\nconfig.insert(&quot;sslmode&quot;, &quot;require&quot;);\n<\/code><\/pre>\n<p>Fine once. Tedious the fourth time. So I wrote a tiny macro:<\/p>\n<pre><code class=\"language-rust\">macro_rules! map {\n    ($($key:expr =&gt; $val:expr),* $(,)?) =&gt; {{\n        let mut m = std::collections::HashMap::new();\n        $( m.insert($key, $val); )*\n        m\n    }};\n}\n<\/code><\/pre>\n<p>And the call site collapses to this:<\/p>\n<pre><code class=\"language-rust\">let config = map! {\n    &quot;host&quot; =&gt; &quot;localhost&quot;,\n    &quot;port&quot; =&gt; &quot;5432&quot;,\n    &quot;user&quot; =&gt; &quot;claude&quot;,\n    &quot;sslmode&quot; =&gt; &quot;require&quot;,\n};\n<\/code><\/pre>\n<p>Three things are worth unpacking, because they&rsquo;re most of what you need to read any <code>macro_rules!<\/code> in the wild:<\/p>\n<ul>\n<li><code>$key:expr<\/code> captures a Rust expression and binds it to <code>$key<\/code>. That <code>:expr<\/code> is a <em>fragment specifier<\/em>. Common ones are <code>expr<\/code>, <code>ident<\/code>, <code>ty<\/code>, <code>tt<\/code>, and <code>literal<\/code>.<\/li>\n<li><code>$( ... ),*<\/code> is repetition. It says &ldquo;match this pattern zero or more times, separated by commas,&rdquo; and the matching <code>$( ... )*<\/code> in the body expands once per capture.<\/li>\n<li><code>$(,)?<\/code> at the end permits an optional trailing comma, so your macro doesn&rsquo;t choke on the comma you always leave after the last line.<\/li>\n<\/ul>\n<p>That&rsquo;s the whole trick. You match a shape, you emit a shape. No separate crate, no <code>syn<\/code>, no build-time gymnastics. When I stopped treating <code>macro_rules!<\/code> as advanced and started treating it as a slightly weird function that takes syntax, it clicked.<\/p>\n<h2 id=\"procedural-macros-powerful-and-mostly-not-yours-to-write\">Procedural macros: powerful, and mostly not yours to write<\/h2>\n<p>Every Rust project you&rsquo;ve touched already leans on procedural macros. <code>#[derive(Serialize, Deserialize)]<\/code> from serde is a derive macro that writes a few hundred lines of serialization code you never see. <code>sqlx::query!<\/code> checks your SQL against a real database at compile time. That&rsquo;s a function-like proc macro doing genuine work before your program ever runs. I got into why that compile-time checking changed how I write data access in my post on <a href=\"https:\/\/abrarqasim.com\/blog\/sqlx-vs-diesel-2026-how-i-pick-my-rust-database-library\" rel=\"noopener\">sqlx vs diesel<\/a>, and the <code>query!<\/code> macro is a big part of why.<\/p>\n<p>Writing your own proc macro is a different sport. You need a separate crate marked <code>proc-macro = true<\/code>, and you parse a <code>TokenStream<\/code> using the <a href=\"https:\/\/docs.rs\/syn\/latest\/syn\/\" rel=\"nofollow noopener\" target=\"_blank\"><code>syn<\/code><\/a> crate, then rebuild output with <code>quote<\/code>. A skeleton derive looks like this:<\/p>\n<pre><code class=\"language-rust\">use proc_macro::TokenStream;\nuse quote::quote;\nuse syn::{parse_macro_input, DeriveInput};\n\n#[proc_macro_derive(Hello)]\npub fn derive_hello(input: TokenStream) -&gt; TokenStream {\n    let ast = parse_macro_input!(input as DeriveInput);\n    let name = &amp;ast.ident;\n    quote! {\n        impl #name {\n            fn hello() { println!(&quot;hi from {}&quot;, stringify!(#name)); }\n        }\n    }.into()\n}\n<\/code><\/pre>\n<p>It&rsquo;s not scary once you&rsquo;ve read it, but notice the cost: an extra crate, two heavy dependencies, and a chunk of compile time on every build. I&rsquo;ve written exactly two proc macros in production, both to remove derive boilerplate across dozens of types where a function couldn&rsquo;t reach. If your use case doesn&rsquo;t look like that, you probably don&rsquo;t need one yet.<\/p>\n<h2 id=\"when-a-macro-is-the-wrong-tool\">When a macro is the wrong tool<\/h2>\n<p>This is the section a year-ago me needed most, because the moment you learn macros everything looks like a nail.<\/p>\n<p>Reach for a plain function or a generic first. Almost always. Functions get real type checking, they show up properly in your IDE, and when they break the error points at your actual mistake. Macros give up a lot of that. When a <code>macro_rules!<\/code> expansion goes wrong, the compiler error often lands <em>inside<\/em> the macro definition, and you&rsquo;re left squinting at where your input actually broke the pattern.<\/p>\n<p>My rough rule: I only write a macro when I&rsquo;m generating <em>syntax<\/em> a function can&rsquo;t produce. Building a <code>HashMap<\/code> with clean literal syntax, implementing the same trait across many types, or capturing an expression unevaluated so I can log it and run it. If the thing you want is &ldquo;run this code with these values,&rdquo; that&rsquo;s a function, and Rust generics probably already cover the &ldquo;works for many types&rdquo; part. Macros hurt readability for the next person, and half the time the next person is you in three months with no memory of the clever expansion you wrote.<\/p>\n<h2 id=\"debugging-macros-without-losing-your-weekend\">Debugging macros without losing your weekend<\/h2>\n<p>The reason macros feel cursed is that you can&rsquo;t see what they expand into. Until you can. Install <code>cargo-expand<\/code> and run:<\/p>\n<pre><code class=\"language-bash\">cargo expand --bin myapp\n<\/code><\/pre>\n<p>It prints your code with every macro fully expanded into plain Rust. The first time I ran it on a serde struct I actually understood what <code>#[derive(Deserialize)]<\/code> had been doing all along, and a bug that had eaten an hour became obvious in about ten seconds. For declarative macros there&rsquo;s also <code>trace_macros!(true);<\/code>, which logs each expansion step as the compiler works through it. Between those two, most of the mystery goes away. If you want to go deep on <code>macro_rules!<\/code> specifically, <a href=\"https:\/\/veykril.github.io\/tlborm\/\" rel=\"nofollow noopener\" target=\"_blank\">The Little Book of Rust Macros<\/a> is the reference I keep open.<\/p>\n<h2 id=\"what-id-actually-do-this-week\">What I&rsquo;d actually do this week<\/h2>\n<p>Don&rsquo;t try to learn the whole macro system. Pick one repeated pattern in a project you already have, the kind of copy-paste that makes you sigh, and write a single <code>macro_rules!<\/code> for it. Then run <code>cargo expand<\/code> and read what it generated. That two-step loop, write a small macro and immediately look at its expansion, is what turned macros from a thing I avoided into a thing I use on purpose. It&rsquo;s the same habit I lean on across the Rust I ship in my <a href=\"https:\/\/abrarqasim.com\/work\" rel=\"noopener\">own projects<\/a>: keep the magic small, and always be able to see through it.<\/p>\n<p>And if you write a macro this week and then delete it next month because a function was clearer, that&rsquo;s not a failure. That&rsquo;s the good ending.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>I avoided Rust macros for a year, then learned them in a weekend. Here is when macro_rules! earns its place, when to skip proc macros, and how I debug them.<\/p>\n","protected":false},"author":2,"featured_media":471,"comment_status":"","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"","rank_math_description":"I avoided Rust macros for a year, then learned them in a weekend. Here is when macro_rules! earns its place, when to skip proc macros, and how I debug them.","rank_math_focus_keyword":"rust macros","rank_math_canonical_url":"","rank_math_robots":"","footnotes":""},"categories":[142],"tags":[538,537,539,64,144],"class_list":["post-472","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-rust","tag-macro-rules","tag-macros","tag-proc-macros","tag-rust","tag-systems-programming"],"_links":{"self":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/472","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/comments?post=472"}],"version-history":[{"count":0,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/472\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media\/471"}],"wp:attachment":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media?parent=472"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/categories?post=472"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/tags?post=472"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}