Macros are more abstract, one meta level up, hence errors are more difficult to relate to code and reason about. They are also more powerful, so errors can have dramatic consequences.
Any kind of code generation setup will have the same characteristics.
Sloppy macros clashing with local names can lead to pretty long debug sessions, but it's the first thing you learn to avoid.
And you're basically inventing your own ad hoc programming language to some extent, the effort spent on error handling tend to not live up to those requirements.
All of that being said, I wouldn't trade them for anything, and I haven't seen any convincing alternatives to Lisp for the full experience.
With care to make the build step "ergonomic", I prefer working with code generation than with macros. Everything about macros is "cleverer" than code generation:
* Implementation of macros require clever algorithms [1] where code generation is mostly straightforward.
* It is harder for IDE's (and most importantly, humans!) to make use of the code because it is not trivial to determine the end result of a macro ("run the code in your head" kind of situation).
There are features that a language can adopt to make it even easier to benefit from code generation, like partial classes [2] and extension methods [3]. Again, these things are a lot more straightforward for both humans and IDEs to understand and work with.
In general, if the macro can't be evaluated to determine its effect, It's not a good macro system. One of the things I like about lisp macros is that you can just apply them to a chunk of code and see what the result would be. Contrast C++ templates, where good luck figuring out either how template application will work or what functions you are actually bind when calling templated code.
When you write macros, you're working one meta level up, since you're writing code that generates code (that generates code etc, but one level higher than usual).
From a user perspective it's different. Macros are more limited; not first class and can't be passed around and called like functions.
If we're talking LISP, I believe "if" is a form, not a macro (because it breaks evaluation rules by only evaluating one branch or the other at runtime depending on the evaluation of the conditional).
In fact, MacCarthy invented a multi-branch conditional construct which in M-expressions looked like
[test1 -> value1; test2 -> value2; T -> default]
The S-expression form was cond:
(cond (test1 value1) (test2 value2) (T default))
The if macro came later, defined in terms of cond.
Macros are no operationally distinguishable from special operators. You know that a form is a macro because there is a binding for the symbol as a macro, and you can ask for the expansion.
Because macros expand to code that may contain special forms that control evaluation, macros thereby exhibit control over evaluation.
It has to be a macro to allow passing the branches as arguments without both being unconditionally evaluated, otherwise you'd have to use lambdas like Smalltalk or lazy eval a la Haskell.
Any kind of code generation setup will have the same characteristics.
Sloppy macros clashing with local names can lead to pretty long debug sessions, but it's the first thing you learn to avoid.
And you're basically inventing your own ad hoc programming language to some extent, the effort spent on error handling tend to not live up to those requirements.
All of that being said, I wouldn't trade them for anything, and I haven't seen any convincing alternatives to Lisp for the full experience.