Rob Elliot
10/22/2022, 10:38 AMtry {
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.Rob Elliot
10/22/2022, 10:39 AMcause
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.Rob Elliot
10/22/2022, 10:40 AMthrow e.cause!!
works fineRob Elliot
10/22/2022, 10:44 AMFleshgrinder
10/22/2022, 10:59 AMcause
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 !!
):
throw e.cause.takeIf { it is AuthenticationException } ?: e
Rob Elliot
10/22/2022, 11:02 AMdata class Thing(var t: Throwable) : Throwable()
val x = Thing(RuntimeException())
if (x.t is IllegalArgumentException) {
throw x.t
}
Rob Elliot
10/22/2022, 11:03 AMtakeIf
is a nice idea, I should use that more.Fleshgrinder
10/22/2022, 11:04 AMthrows
, 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.
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:
if (e.getCause() instanceof AuthenticationException) {
throw e.getCause(); // is it returning the same? 🤔
}
throw e;
Rob Elliot
10/22/2022, 11:05 AMThrowable?
) is not compatible with throws
.Rob Elliot
10/22/2022, 11:05 AMFleshgrinder
10/22/2022, 11:05 AMSmart cast to 'AuthenticationException' is impossible, because 'e.cause' is a property that has open or custom getter
Fleshgrinder
10/22/2022, 11:08 AM!!
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:
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) // 😑
}
Rob Elliot
10/22/2022, 11:08 AMAuthenticationException
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
.Fleshgrinder
10/22/2022, 11:08 AMis
check, that's where the story starts.Rob Elliot
10/22/2022, 11:12 AMType mismatch.
Required: Throwable
Found: Throwable?
Smart cast to 'AuthenticationException' is impossible, because 'e.cause' is a property that has open or custom getter`
Fleshgrinder
10/22/2022, 11:14 AMRob Elliot
10/22/2022, 11:19 AMFleshgrinder
10/22/2022, 11:25 AMtakeIf
does this without any verbosity. Of course you can do it manually too (as previously shown).ephemient
10/22/2022, 2:36 PMNote 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 codeRob Elliot
10/25/2022, 11:30 AM