Macro Tips: Avoid nesting macros if possible
Tags: crystal, macros, lucky
Published: 2020-12-30 05:46:01

One issue that has come up recently is that error output for compilations errors only contains the place the error occurred and the place that called the offending code. It might not sound like a problem, but pair that with macros and it will all become clear. It's easiest to show, so here's some code that produces a compilation error:

```crystal
macro compilation_error(arg)
  1 < {{ arg }}
end

puts compilation_error("asdf")
```

and the compilation error looks like:

```bash
Showing last frame. Use --error-trace for full trace.

There was a problem expanding macro 'compilation_error'

Code in temp.cr:10:6

 10 | puts compilation_error("asdf")
           ^
Called macro defined in temp.cr:2:1

 2 | macro compilation_error(arg)

Which expanded to:

 > 1 | 1 < "asdf"
         ^
Error: no overload matches 'Int32#<' with type String

Overloads are:
 ...cut for brevity
```

The error output for this compilation error is kind of nice. It states there's an issue and then the very first code reference is to where the macro was called. The next code reference points towards the macro-generated code that caused the error. If the user is not familiar with what code is generated by the macro they are using (and maybe they don't even know it's a macro), the first code reference is often way more helpful than when it dives into the macro code.

Now, let's change how the macro works just a little bit and see what happens to the error output.

```crystal
macro inner_compilation_error(arg)
  1 < {{ arg }}
end

macro compilation_error(arg)
  inner_compilation_error({{ arg }})
end

puts compilation_error("asdf")
```

And the new error output

```bash
Showing last frame. Use --error-trace for full trace.

There was a problem expanding macro 'inner_compilation_error'

Code in macro 'compilation_error'

 1 | inner_compilation_error("asdf")
     ^
Called macro defined in eval:1:1

Which expanded to:

 > 1 | 1 < "asdf"
         ^
Error: no overload matches 'Int32#<' with type String

Overloads are:
 ...cut for brevity
```

Can you notice the difference? Where in the error output does it point towards where the macro was called? The answer, it doesn't.

The change to the code was adding another macro method that the original macro method now delegates to. This is very common when making macros since the code generation can become complicated. The downside is that error output only shows the compilation error and the code that called it, and in this case it means that it never makes it out of the macro code. Not so bad when you manage and call the macro, annoying and confusing when you are calling macros provided to you from libraries. At least in this code example you can kind of reference that the error is because you passed in a string, but what if the argument passed into the top-level macro is augmented in some way? What if the string is split into an array? I believe the error output becomes confusing and of little value. (I've written this article without talking about `--error-trace` because I believe that errors should be helpful without it)

## Story from Lucky-land

In [Lucky](https://luckyframework.org/) we generate URL helper methods to make it easier to create links in HTML, and we do this using macros called when users set up their actions (what we call controllers in a traditional MVC architecture). A common request for help in our Discord is to help figure out where users went wrong with their action setup. They copy and paste their action code and provide a picture of the stack-trace that looks something like this

![Old stack-trace output](https://user-images.githubusercontent.com/17329408/103235223-86cf4600-490f-11eb-8737-67033116207a.png)

You'll notice that the image of the stack-trace looks a little like the second example I laid out above. It talks about an error but the code references it points at are all in macro code. That's why user's end up staring at their action and are forced to ask for help. The awful thing about it is that the error isn't in their action at all. The user spends the most time staring at the action to see the error, but anyone who tries to help also has to spend time carefully reading the code. The annoying thing is that the real problem was with their usage of the generated URL helpers I mentioned, and not with the action implementation at all.

Because I had experienced this issue myself and had helped people out with this often, I really wanted to figure out how to get the usage site (where they originally called into macro code) into the error output. Without going into the code, I looked at the macro and saw something very similar to the code example above where the main macro delegated to another but nowhere else was this second macro called. Moving the second macro code into the first was the solution and now the usage site shows up in the error output!

![New stack-trace output](https://user-images.githubusercontent.com/17329408/103235233-8a62cd00-490f-11eb-96ec-7282a352cb25.png)

Here's the pull request that made the change if you want to reference it {% github https://github.com/luckyframework/lucky/pull/1373 %}