Should a macro throw an error, or return expression that throws an error?

I have a macro that in some circumstances must throw an error. Is there a recommended way of doing that?

For example, I can throw the error directly from the macro, or return code that throws the error.

macro A()
    if true
        error("Error message")
    else
        return :(1 + 1)
    end
end

macro B()
    if true
        return quote
            error("Error message")
        end
    else
        return :(1 + 1)
    end
end

If I understand it correctly, macro @A would cause an error during compile time, while macro @B would cause an error during run time. I don’t think this would make any difference to the user who calls my macro in their code. However, it makes a difference for me in my unit tests. In particular, I can do

julia> @test_throws ErrorException  @B
Test Passed
  Expression: #= REPL[5]:1 =# @B
      Thrown: ErrorException

But I cannot test @A the same way, because it doesn’t even compile

julia> @test_throws ErrorException @A
ERROR: Error message
Stacktrace:
  [1] error(s::String)
    @ Base ./error.jl:33
  [2] var"@A"(__source__::LineNumberNode, __module__::Module)
      . . .

I’m hoping to have discussion about this. Would you recommend one versus the other and why? Am I thinking about this correctly? Are there other considerations that I should take into account?

I think that you have actually answered yourself:

That does make a difference for users too. Let’s imagine that the macro is not used interactively or in global scope, but inside a function. Then @A would throw the error in the moment of defining the function (it does not even need to be compiled), but @B would let the function exist, which would fail in the moment of calling it.

You have to decide if you need one thing or the other.

Use

@test_throws LoadError @eval @A

but I think that only allows to capture LoadErrors.

julia> macro errormacro(ex)
         throw(ArgumentError("wrong argument"))
       end
@errormacro (macro with 1 method)

julia> Test.@test_throws ArgumentError @macroexpand @errormacro 2
Test Passed
      Thrown: ArgumentError

Tiny caveat: Works only from Julia 1.7 on
From the docstring of LoadError:

LoadErrors are no longer emitted by @macroexpand, @macroexpand1, and macroexpand as of Julia 1.7.

2 Likes

If the error can be thrown at compile-time, I’d say it’s best to fail during macro expansion. Examples of early macro expansion failure include the Test macros themselves:

julia> using Test

julia> @macroexpand @test true unsupported_kwarg=nothing
ERROR: invalid test macro call: @test true unsupported_kwarg = nothing

This example also illustrates how @macroexpand allows to clearly separate the macro expansion stage from the runtime, which allows testing for errors that happen during macro expansion:

julia> macro A()
           error("ko")
       end
@A (macro with 1 method)

julia> @test_throws ErrorException @macroexpand @A
Test Passed
      Thrown: ErrorException
3 Likes

Thanks everyone for your replies and ideas. I learned about using @macroexpand for this test and this is very helpful, thanks again!

Btw, I tested it and it doesn’t work in Julia 1.6, but it works great in 1.7 and 1.8.

Another way of thinking about it is that macros are basically syntax transformations. So it makes sense to throw errors as soon as possible (like syntax errors)

1 Like