Mildly irritating: ```try { TODO() } catch (e: E...
# compiler
r
Mildly irritating:
Copy code
try {
  TODO()
} catch (e: Exception) {
  if (e.cause is AuthenticationException) {
    throw e.cause // does not compile!
  }
  throw e
}
Gives me compiler error
Smart cast to 'AuthenticationException' is impossible, because 'e.cause' is a property that has open or custom getter
. But all I'm doing with it is throwing it, and the only requirement of
throw
is that it's a
Throwable
- which it definitely is.
And I've instantly worked out the reason -
cause
is not of type
Throwable
, it's of type
Throwable?
so it could have been set to
null
in the meantime, in which case you can't throw it. So fair enough, compiler.
throw e.cause!!
works fine
I'm going to salve my ego by suggesting the compiler error could have been clearer about it being the nullability that's the issue.
f
The nullability is not the issue, the issue is that the value of
cause
might have changed between you checking for what it is and actually using it. A possible way to ensure that this cannot happen (there are many, including your
!!
):
Copy code
throw e.cause.takeIf { it is AuthenticationException } ?: e
r
Pretty sure the nullability is the issue. This compiles and works fine:
Copy code
data class Thing(var t: Throwable) : Throwable()

val x = Thing(RuntimeException())

if (x.t is IllegalArgumentException) {
    throw x.t
}
But
takeIf
is a nice idea, I should use that more.
f
The nullability is why you cannot use it with
throws
, yes, but the compiler error is about something entirely different, since it is smarter than that. It does not complain about the null usage with
throws
, it complains about the fact that you properly checked what the type is, but then call the getter again, and now the result might be different.
Copy code
val cause: Throwable? = e.cause
if (cause is AuthenticationException) {
    throw cause
}
throw e
This works, since
cause
is on our stack and cannot suddenly change. Kotlin hides the difference between property access and getter calls, which leads to code that constantly calls getter functions. The problem is that there is no guarantee that a getter returns the same data if called twice (plus the overhead of calling something over and over again). Java:
Copy code
if (e.getCause() instanceof AuthenticationException) {
    throw e.getCause(); // is it returning the same? 🤔
}
throw e;
r
Yes, but the reason it then complains is that the resulting type (
Throwable?
) is not compatible with
throws
.
i.e. it's nullable.
f
That's not what you quoted above:
Smart cast to 'AuthenticationException' is impossible, because 'e.cause' is a property that has open or custom getter
Ah, you mean again that you would expect it to give you that error message. Well, it could, but it would less intelligent. It tries to take care of the casting for you, since you properly checked the type, and it fails to do so, because of what I explained. Tacking on
!!
is not 100% the same result, since now you throw a
Throwable
and not the desired
AuthenticationException
. Obviously in this particular case there is no difference, but consider this:
Copy code
fun f(ae: AuthenticationException) {}

// snip

if (e.cause is AuthenticationException) {
  f(e.cause) // Expected AuthenticationException but got Throwable?
  f(e.cause!!) // Expected AuthenticationException but got Throwable
  f(e.cause as AuthenticationException) // 😑
}
r
My point is that the reason it tries to smart cast to
AuthenticationException
is that
Throwable?
is not compatible with
throws
. In my second example it's perfectly happy not to smart cast at all because even if
x.t
is no longer an
IllegalArgumentException
it's still a
Throwable
.
f
Sure, and your
is
check, that's where the story starts.
r
Ideally it would say something like:
Copy code
Type mismatch.
  Required: Throwable
  Found:    Throwable?
Smart cast to 'AuthenticationException' is impossible, because 'e.cause' is a property that has open or custom getter`
f
Agree, more context is better. Ideally it would include the exact location, with the code, highlighting of the column, and a tip on how to fix it. Rust is way ahead here to almost all other languages.
r
I think in this case the explanation about smart casting is the context - the real error is the type mismatch.
f
Well, depends, Kotlin is trying to educate here. Calling getters multiple times is simply a terrible idea. Get the value on the stack, and then there are no problems.
takeIf
does this without any verbosity. Of course you can do it manually too (as previously shown).
e
https://kotlinlang.org/docs/typecasts.html#smart-casts
Note that smart casts work only when the compiler can guarantee that the variable won't change between the check and the usage.
this obviously isn't the case for
e.cause
which is
e.getCause()
which is some unknown external code
r
I wasn't querying the smart cast, I totally understood why it couldn't smart cast, I was confused as to why it couldn't throw despite being unable to smart cast. I realised why just after I posted.