# Compiler support on production ready error codes

**URL:** <https://forum.crystal-lang.org/t/compiler-support-on-production-ready-error-codes/6087>\
**Category:** Help & Support\
**Created:** [October 14, 2023, 5:39pm UTC](https://forum.crystal-lang.org/t/compiler-support-on-production-ready-error-codes/6087 "2023-10-14T17:39:58Z")\
**Posts on this page:** 5\
**Page:** 1

<div class="post-metadata">

**Author:** ![wolfgang371](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/wolfgang371/32/1665_2.png) [@wolfgang371](https://forum.crystal-lang.org/u/wolfgang371)\
**Post date:** [October 14, 2023, 5:39pm UTC](https://forum.crystal-lang.org/t/compiler-support-on-production-ready-error-codes/6087/1 "2023-10-14T17:39:58Z")

</div>

I was wondering if Crystal could fully handle the following:

1. identify all `assert`ions in the code
2. number them consecutively (“error code”)
3. if any of the `assert`ions fails, it has to fail with its error code (this is for the end user)
4. in addition create a file with a full mapping of all “error codes” to “source files and lines” (this is for the author only)

This is as far as I could get:

```crystal
ERROR_CODEFILE = "generated-errorcodes.txt"
{% system("rm #{ERROR_CODEFILE}; true") %}
ENV["XX"] = "0" # since assert() always increments first, we effectively start with "1"

macro assert(invariant, f= __FILE__ , l= __LINE__ )
    ENV["XX"] = (ENV["XX"].to_i+1).to_s
    {% system("echo #{f}, #{l} >> #{ERROR_CODEFILE}") %}
    {% if invariant %}
        if {{invariant}} # see https://github.com/crystal-lang/crystal/issues/13209
        else
            raise("error code ##{ENV["XX"]}")
        end
    {% else %}
        raise("error code ##{ENV["XX"]}")
    {% end %}
end

assert(false)
assert(false)

```

I get something like this after build:

```plaintext
# generated-errorcodes.txt
.../macro.cr, 18
.../macro.cr, 19

```

And this after running:

```plaintext
Unhandled exception: error code #1 (Exception)

```

So, I practically have only those two points open:

1. I couldn’t get Crystal to put the numbers in the file as well
2. my solution somehow feels clumsy

Any ideas? Thanks! 🙂

---

<div class="post-metadata">

**Author:** ![tsornson](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/tsornson/32/1651_2.png) [@tsornson](https://forum.crystal-lang.org/u/tsornson)\
**Post date:** [October 14, 2023, 6:01pm UTC](https://forum.crystal-lang.org/t/compiler-support-on-production-ready-error-codes/6087/2 "2023-10-14T18:01:35Z")

</div>

Neat use case! It looks like you’re storing and using the error code at runtime, when “hardcoding” the error code at compile time will probably tidy things up a bit. Also, we can take advantage that the macro language can manipulate its own variables too (including constants, to a degree):

```auto
ERROR_CODE_PROPERTIES = {
    file_name: "generated-errorcodes.txt",
    current_code: 0
}

{% system("rm #{ERROR_CODE_PROPERTIES[:file_name]}; true") %}

macro assert(invariant, f= __FILE__ , l= __LINE__ )
    {%
        ERROR_CODE_PROPERTIES[:current_code] += 1
        system("echo #{f}, #{l}, #{ERROR_CODE_PROPERTIES[:current_code]} >> #{ERROR_CODE_PROPERTIES[:file_name]}")
    %}
    raise("error code #{{ERROR_CODE_PROPERTIES[:current_code]}}") unless {{invariant}}
end

assert(false)
assert(false)

```

Or something like that. It can probably be cleaned up a bit more too, and I haven’t tested the above either, but have written macros that update and modify constants in this way before at compile time.

---

<div class="post-metadata">

**Author:** ![wolfgang371](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/wolfgang371/32/1665_2.png) [@wolfgang371](https://forum.crystal-lang.org/u/wolfgang371)\
**Post date:** [October 15, 2023, 8:11am UTC](https://forum.crystal-lang.org/t/compiler-support-on-production-ready-error-codes/6087/3 "2023-10-15T08:11:33Z")

</div>

Thanks a lot, this definitely looks way better! It also works for my simple examples.

And, of course, there’s a “but” (actually it’s two 😉):

1. I need to tweak the assert logic the way I did it in my first post, but that’s a different story
2. this is the bigger one: it breaks the compiler once more:

```crystal
ERROR_CODE_PROPERTIES = {
    file_name: "generated-errorcodes.txt",
    current_code: 0
}
{% system("rm #{ERROR_CODE_PROPERTIES[:file_name]}; true") %}
macro assert(invariant, f= __FILE__ , l= __LINE__ )
    {%
        ERROR_CODE_PROPERTIES[:current_code] += 1
        system("echo \"#{ERROR_CODE_PROPERTIES[:current_code]}; #{f}; #{l}\" >> #{ERROR_CODE_PROPERTIES[:file_name]}")
    %}
    {% if invariant %}
        if {{invariant}} # see https://github.com/crystal-lang/crystal/issues/13209
        else
            raise("error code #{ERROR_CODE_PROPERTIES[:current_code]}")
        end
    {% else %}
        raise("error code #{ERROR_CODE_PROPERTIES[:current_code]}")
    {% end %}
end

class X
    def initialize
    end
    def do(&block)
        assert(false)
    end
end

x = X.new
x.do {}
x.do {}

```

… gives…

```plaintext
Module validation failed: GEP base pointer is not a vector or a vector of pointers
  %42 = getelementptr inbounds %"NamedTuple(file_name: String, current_code: Int32)", i8 %41, i32 0, i32 1, !dbg !97
 (Exception)
  from /crystal/src/llvm/module.cr:73:9 in 'codegen'
  from /crystal/src/compiler/crystal/compiler.cr:172:16 in 'compile:combine_rpath'
  from /crystal/src/compiler/crystal/compiler.cr:165:56 in 'compile:combine_rpath'
  from /crystal/src/compiler/crystal/command.cr:227:5 in 'run_command'
  from /crystal/src/compiler/crystal/command.cr:125:10 in 'run'
  from /crystal/src/compiler/crystal.cr:11:1 in '__crystal_main'
  from /crystal/src/crystal/main.cr:129:5 in 'main'
  from src/env/__libc_start_main.c:95:2 in 'libc_start_main_stage2'
Error: you've found a bug in the Crystal compiler. Please open an issue, including source code that will allow us to reproduce the bug: https://github.com/crystal-lang/crystal/issues

```

(showing up on both 1.9.2 and 1.10.1, Ubuntu 18.04)

I’ll post it on crystal…

---

<div class="post-metadata">

**Author:** ![wolfgang371](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/wolfgang371/32/1665_2.png) [@wolfgang371](https://forum.crystal-lang.org/u/wolfgang371)\
**Post date:** [October 15, 2023, 10:56am UTC](https://forum.crystal-lang.org/t/compiler-support-on-production-ready-error-codes/6087/4 "2023-10-15T10:56:21Z")

</div>

If you’re curious about my “but #1” from above, here is some code:

```crystal
ERROR_CODE_PROPERTIES = {
    file_name: "generated-errorcodes.txt",
    current_code: 0
}

{% system("rm #{ERROR_CODE_PROPERTIES[:file_name]}; true") %}

# -------------- three different implementations for `assert` follow

# variant A: this yields a compiler error message for some use cases (see tests below)
macro assert(invariant, f= __FILE__ , l= __LINE__ )
    {%
        ERROR_CODE_PROPERTIES[:current_code] += 1
        system("echo #{f}, #{l}, #{ERROR_CODE_PROPERTIES[:current_code]} >> #{ERROR_CODE_PROPERTIES[:file_name]}")
    %}
    raise("error code #{{ERROR_CODE_PROPERTIES[:current_code]}}") unless {{invariant}}
end

# # variant B: this macro works for all tests below
# macro assert(invariant, f= __FILE__ , l= __LINE__ )
# {% if invariant %}
# if {{invariant}} # see https://github.com/crystal-lang/crystal/issues/13209
# else
# raise("error code #{ERROR_CODE_PROPERTIES[:current_code]}")
# end
# {% else %}
# raise("error code #{ERROR_CODE_PROPERTIES[:current_code]}")
# {% end %}
# end

# # variant C: this macro crashes compiler for some use cases (see tests below)
# macro assert(invariant, f= __FILE__ , l= __LINE__ )
# {%
# ERROR_CODE_PROPERTIES[:current_code] += 1
# system("echo \"#{ERROR_CODE_PROPERTIES[:current_code]}; #{f}; #{l}\" >> #{ERROR_CODE_PROPERTIES[:file_name]}")
# %}
# {% if invariant %}
# if {{invariant}} # see https://github.com/crystal-lang/crystal/issues/13209
# else
# raise("error code #{ERROR_CODE_PROPERTIES[:current_code]}")
# end
# {% else %}
# raise("error code #{ERROR_CODE_PROPERTIES[:current_code]}")
# {% end %}
# end

# ------------------- some tests follow

def x(arg : Int32|Nil) : Int32
    assert(!arg.nil?) # this compiles with all three variants
    arg + 1
end

def y : Int32
    # raise("foo") # this compiles with all three variants
    assert(false) # this only compiles with variant B (error on A, crash on C)
end

x(ARGV.size==0 ? nil : 42)
y

assert(false)
assert(false)

```

---

<div class="post-metadata">

**Author:** ![wolfgang371](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/wolfgang371/32/1665_2.png) [@wolfgang371](https://forum.crystal-lang.org/u/wolfgang371)\
**Post date:** [August 22, 2025, 8:16pm UTC](https://forum.crystal-lang.org/t/compiler-support-on-production-ready-error-codes/6087/5 "2025-08-22T20:16:54Z")

</div>

Recently I revisited this code fragment just to figure out it doesn’t really work.

This code now does (for Linuxes):

```auto
ERROR_CODEFILE = "generated-errorcodes.txt"
{% system("echo code, file, line > #{ERROR_CODEFILE}") %}
macro assert(invariant, f= __FILE__ , l= __LINE__ )
    # the only way to have "static" compile time variables:
    {% counter = read_file(ERROR_CODEFILE).lines.size %}
    {% system("echo #{counter}, #{f}, #{l} >> #{ERROR_CODEFILE}") %}
    # see https://forum.crystal-lang.org/t/run-time-assertions-and-type-reductions/4694
    # needs to be a macro, method doesn't do compile time type reductions
    # compile time "if" is necessary...
    {% if invariant %}
        if {{invariant}} # see https://github.com/crystal-lang/crystal/issues/13209
        else
            raise("error code # {{counter}}")
        end
    {% else %}
        raise("error code # {{counter}}")
    {% end %}
end

assert(true)
assert(false) # yields Unhandled exception: error code # 2 (Exception)

# cat generated-errorcodes.txt # yields
# code, file, line
# 1, .../assert.cr, 20
# 2, .../assert.cr, 21

```

Seems to me missing Crystal compile time variables can be coped for with files…
