As I was implementing an Exception subtype for the first time, I looked at some implementations in Base, for example ArgumentError. I was surprised by two things:
a lot of the subtypes are mutable
a lot of the subtypes don’t have concretely typed fields
On the other hand, the documentation refers to UndefVarError as an example of how to write a custom exception type, which is an immutable struct without abstract fields.
mutable is probably just there for historical reason. You don’t need to type the field though. It’s just and error and there’s almost no reason to type the field.
I think the reason is that performance does not matter a lot since exceptions are only used in slow paths anyway, and as long as they are thrown from a separate @noinline function they does not slow the common path (as you noticed). But making them immutable would likely be a good idea. OTC, adding type parameters for fields is a bit more invasive and would slow down compilation by specializing on these types, so it might not be worth it.
Thanks. I was just wondering if not concretely typing everything could make the non-throwing path slightly slower as well, but it seems that that is not the case. For:
struct ExceptionA <: Exception
msg::String
end
mutable struct ExceptionB <: Exception
msg::AbstractString
end
@noinline function a(x)
x <= 0 || throw(ExceptionA("x > 0"))
end
@noinline function b(x)
x <= 0 || throw(ExceptionB("x > 0"))
end
@noinline throw_a() = throw(ExceptionA("x > 0"))
@noinline function a2(x)
x <= 0 || throw_a()
end