# Should \`esc\` work for the macro name of a \`macrocall\` expression?

**URL:** <https://discourse.julialang.org/t/should-esc-work-for-the-macro-name-of-a-macrocall-expression/134817>\
**Category:** General Usage\
**Created:** [December 31, 2025, 5:04pm UTC](https://discourse.julialang.org/t/should-esc-work-for-the-macro-name-of-a-macrocall-expression/134817 "2025-12-31T17:04:30Z")\
**Posts on this page:** 5\
**Page:** 1

<div class="post-metadata">

**Author:** ![matthias314](https://avatars.discourse-cdn.com/v4/letter/m/a88e4f/32.png) [@matthias314](https://discourse.julialang.org/u/matthias314)\
**Post date:** [December 31, 2025, 5:04pm UTC](https://discourse.julialang.org/t/should-esc-work-for-the-macro-name-of-a-macrocall-expression/134817/1 "2025-12-31T17:04:30Z")

</div>

It seems that a macro returning a `:macrocall` expression with escaped macro name does not work whereas a `:call` expression with escaped function name is no problem:

```julia-auto
julia> macro a(ex::Expr) Expr(ex.head, esc.(ex.args)...) end
@a (macro with 1 method)

julia> @a one(0)
1

julia> @a @inbounds 0
ERROR: LoadError: syntax: "esc(...)" used outside of macro expansion
Stacktrace:
 [1] top-level scope
   @ REPL[3]:1
in expression starting at <macrocall>:0

```

> **Same example with dumped expressions**
>
> ```julia-auto
> julia> macro a(ex::Expr) escex = Expr(ex.head, esc.(ex.args)...); dump(escex); escex end
> @a (macro with 1 method)
> 
> julia> @a one(0)
> Expr
> head: Symbol call
> args: Array{Any}((2,))
> 1: Expr
> head: Symbol escape
> args: Array{Any}((1,))
> 1: Symbol one
> 2: Expr
> head: Symbol escape
> args: Array{Any}((1,))
> 1: Int64 0
> 1
> 
> julia> @a @inbounds 0
> Expr
> head: Symbol macrocall
> args: Array{Any}((3,))
> 1: Expr
> head: Symbol escape
> args: Array{Any}((1,))
> 1: Symbol @inbounds
> 2: Expr
> head: Symbol escape
> args: Array{Any}((1,))
> 1: LineNumberNode
> line: Int64 1
> file: Symbol REPL[3]
> 3: Expr
> head: Symbol escape
> args: Array{Any}((1,))
> 1: Int64 0
> ERROR: LoadError: syntax: "esc(...)" used outside of macro expansion
> Stacktrace:
> [1] top-level scope
> @ REPL[3]:1
> in expression starting at <macrocall>:0
> 
> julia> @macroexpand1 @a @inbounds 0
> Expr
> head: Symbol macrocall
> args: Array{Any}((3,))
> 1: Expr
> head: Symbol escape
> args: Array{Any}((1,))
> 1: Symbol @inbounds
> 2: Expr
> head: Symbol escape
> args: Array{Any}((1,))
> 1: LineNumberNode
> line: Int64 1
> file: Symbol REPL[4]
> 3: Expr
> head: Symbol escape
> args: Array{Any}((1,))
> 1: Int64 0
> :($(Expr(:escape, Symbol("@inbounds"))) $(Expr(:escape, 0)))
> 
> ```

Is there a reason for this?

---

<div class="post-metadata">

**Author:** ![Benny](https://avatars.discourse-cdn.com/v4/letter/b/49beb7/32.png) [@Benny](https://discourse.julialang.org/u/Benny)\
**Post date:** [January 1, 2026, 4:15pm UTC](https://discourse.julialang.org/t/should-esc-work-for-the-macro-name-of-a-macrocall-expression/134817/2 "2026-01-01T16:15:17Z")

</div>

It works if the macro symbol isn’t escaped, so I assume `Expr(:escape, Symbol("@inbounds"))` ends up in the wrong `eval` call somewhere. No idea why this shouldn’t work though.

```julia-auto
julia> function b(ex::Expr) Expr(ex.head, ex.args[1], esc.(ex.args[2:end])...) end

julia> macro b(ex::Expr) b(ex) end
@b (macro with 1 method)

julia> @b @inbounds one(0)
1

```

---

<div class="post-metadata">

**Author:** ![mlechu](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mlechu/32/215203_2.png) [@mlechu](https://discourse.julialang.org/u/mlechu)\
**Post date:** [January 3, 2026, 1:46am UTC](https://discourse.julialang.org/t/should-esc-work-for-the-macro-name-of-a-macrocall-expression/134817/3 "2026-01-03T01:46:49Z")

</div>

Good question. There are two reasons for this that I could think of:

1. It’s a rare thing to want. The first argument `mac_expr` in an `Expr(:macrocall, mac_expr, args...)` is evaluated at top level (locals require lowering), so the only thing `Expr(:escape)` could change here is the module `mac_expr` is evaluated into.
2. The way it’s currently implemented, macro expansion runs recursively and evaluates every `mac_expr` before the `Expr(:escape)`s are handled in a second pass.

Still, I think this could be made to work. In your example, the escape would cause macro expansion to resolve `@inbounds` in the module of the code calling `@a`, instead of the module where `@a` is defined.

---

<div class="post-metadata">

**Author:** ![matthias314](https://avatars.discourse-cdn.com/v4/letter/m/a88e4f/32.png) [@matthias314](https://discourse.julialang.org/u/matthias314)\
**Post date:** [January 3, 2026, 2:03am UTC](https://discourse.julialang.org/t/should-esc-work-for-the-macro-name-of-a-macrocall-expression/134817/4 "2026-01-03T02:03:20Z")

</div>

> [@mlechu](#):
>
> I think this could be made to work

Do you mean by modifying Julia internals? I think it would be useful to be able to change the module where a macro is expanded.

(I believe that @c42f is working on a more general overhaul of the macro system. Changing the “old” macro code might therefore not be a priority.)

---

<div class="post-metadata">

**Author:** ![c42f](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/c42f/32/52842_2.png) [@c42f](https://discourse.julialang.org/u/c42f)\
**Post date:** [January 3, 2026, 5:59am UTC](https://discourse.julialang.org/t/should-esc-work-for-the-macro-name-of-a-macrocall-expression/134817/5 "2026-01-03T05:59:46Z")

</div>

> [@matthias314](#):
>
> Do you mean by modifying Julia internals?

Yes - the internals would require changing to make this work in a sane way. There’s a somewhat-functional workaround using the `hygienic-scope` head instead of `Expr(:escape)` as follows:

```julia-auto
julia> module A
           macro inbounds(ex)
               "lol surprise! Not Base.@inbounds"
           end
           
           macro a(ex::Expr)
               Expr(ex.head, Expr(:var"hygienic-scope", ex.args[1], __module__ ), esc.(ex.args[2:end])...)
           end
           
           macro b(ex::Expr)
               Expr(ex.head, ex.args[1], esc.(ex.args[2:end])...)
           end
       end
Main.A

julia> A.@a @inbounds 1
1

julia> A.@b @inbounds 1
"lol surprise! Not Base.@inbounds"

```

Note this is a bit of a hack because it’s not composable when the macro name or function call contains `esc()` itself. (I’m also unsure how much the `hygienic-scope` head is considered a public API vs a detail of macro expansion. At this point it might be stable I guess!)

I am working on overhauling macros (@mlechu is working on this too!). In the new system `esc()` isn’t required here and it should already work as expected.

The old macro system isn’t going away though - it will just work compatibly alongside the new system which people can opt into.
