DEV Community

Matthew McGarvey
Matthew McGarvey

Posted on

Macro Tips: Method usage compilation errors

One cool feature of Crystal macros that I forget about is that they don't always have to be defined in a macro method. Where I've seen this be helpful is when you want to define a compile-time error in a method.

One way to think about it is, say you have a method called destroy and people keep mentioning that they keep forgetting and accidentally trying to call delete. Sometimes Crystal gives you a recommendation of the method it thinks you wanted but that is not always the case. To be helpful to users, you could use macros to provide a better error.

class MyLibrary
  def destroy
    # pure destruction

  def delete
    {% raise "Woops! Did you mean to call destroy instead?" %}

my_library =
my_library.delete #=> Helpful compilation error!
Enter fullscreen mode Exit fullscreen mode

It's a nice way to provide help to users who make mistakes from time to time. We use this in Lucky's ORM, Avram, to help users avoid making mistakes that would otherwise produce hard to understand compilation errors, or, even worse, runtime errors 🙀. One example is that in Avram::Model you define a "belongs to" association by writing belongs_to user : User but in a migration you can add a column with a foreign key relationship to a different table by writing add_belongs_to user : User. Out of habit, I have accidentally used belongs_to instead of add_belongs_to in a migration and didn't get an understandable error. It took me several minutes to track it down and I felt dumb for making the mistake. Because of that experience, we added a compilation error using macros to catch that and point users towards the solution. Here's the code in Avram that can be found on GitHub here (just a note that this is in a macro def but the same thing can be done in a regular def as well).

macro belongs_to(type_declaration, *args, **named_args)
  {% raise <<-ERROR
    Unexpected call to `belongs_to` in a migration.
    Found in #{}:#{type_declaration.line_number}:#{type_declaration.column_number}.
    Did you mean to use 'add_belongs_to'?
    'add_belongs_to #{type_declaration}, ...'
Enter fullscreen mode Exit fullscreen mode


One problem we've had with this recently, though, is that we implemented this in the base method of a class that would be inherited and a module dynamically overrides it to provide the expected functionality. Because we were replacing existing code, some users were calling super in their method expecting to get the base functionality and were instead getting a compilation error. This is not a knock of the macro usage, just a note that it isn't always the perfect choice.

Top comments (0)