Extension functions: IllegalArgumentException vs. IllegalStateException

Given:

class C(val s: String?)

I you consider an extension function just syntactic sugar for a function that takes an object as a parameter, you would do this:

fun C.foo() {
    if (s == null)
        throw IllegalArgumentException("C is illegal because it does not have s.")
}

// Or:

fun C.foo() {
    requireNotNull(s) { "C is illegal because it does not have s." }
}

But when you view it as a method on an object, you would do this:

fun C.foo() {
    if (s == null)
        throw IllegalStateException("You cannot call foo() when s is null.")
}

// Or:

fun C.foo() {
    checkNotNull(s) { "You cannot call foo() when s is null." }
}

So, which one should you use?

2 Likes

I don’t think there is a right and wrong here as long as you stay consistent. I would probably use an IllegalStateException because I think of extension functions as a kind of “member” function. That said your argument to treat the receiver as an argument is also valid.

2 Likes

I think the way that the receiver is called this within the function, as well as the dot notation for calling it, are strong invitations to think of extension functions in a similar way to member functions (i.e. methods).

Yes, they’re implemented as static methods in the bytecode, but I think that’s more a reflection of JVM limitations, and for interoperability, than any statement of intent. It’s similar in Java: in my experience, most Java static methods would be written as extensions if that was an option.

So I think the arguments for treating them like methods are much stronger, and for me an IllegalStateException would feel more natural.

1 Like

This is an interesting point that could be taken for an argument for IllegalArgumentException as well. If you write a library that is used from java (and let’s face it, java is still much more used than kotlin sadly) then throwing an IllegalArgumentException might be the way to go, to make the interop with java even smoother.
And even with me bringing up this argument I personally think IllegalStateException is the way to go.

I think it varies depending on the function is and use-case. For me, this is all about communicating to the user as effectively as possible.

I usually land on illegal state exceptions for extension functions.

Although extension functions are statically resolved, they still feel like a property of instances of their reciever’s type–similar to how member functions are exactly that, but in addition to that member functions are dynamically dispatched.

Just to round out the idea: you could just as easily imagine member functions to be functions with their reciever as a parameter (with dynamical dispatch)–then you’d be asking this question about member functions.

2 Likes

When I look at both exceptions I would see IllegalArgumentException as a usage error, but IllegalStateException as also a design error (sometimes not even a usage error). In principle, you would aim to design classes such that they only have a single (behaviour relevant) state. Even with such a design it can be possible that the state is inconsistent/corrupt, but that would be an indication of an implementation bug (somewhere). Of course there are cases where there must be state, but often subclasses/generics/interfaces can be used to present a typesafe interface that encapsulates this state in a way that the compiler can check.

An IllegalArgumentException indicates to me that the caller is violating the function contract. So in the case that you have an extension function on a list and it doesn’t work on an empty list (by design) that is not a state issue, it is a value issue. The argument doesn’t fit the expected domain. At the same time I can see where you want parity between regular and extension functions and be consistent.

2 Likes

Well said @pdvrieze :+1: I hadn’t looked at it quite that way.

So if one chooses via those guidelines, what is chosen for a class with initialization (no extension functions)? Failing to call MyClass.initialize() before MyClass.doSomething() is a usage error. But I would consider this example to be one for an IllegalStateException.
Do you tend to stick with your guidelines for choosing or do you flex on this case?

A part of me wishes we had an IllegalUsageException instead of IllegalArgumentException.

@arocnies Beyond that you would attempt to avoid classes with initializer functions (one way is to have two types (possibly sharing private state), one is the creator, the other the initialized object - they could even share an interface where appropriate). However, in the case that it is unavoidable (like a fixed ABI) I would go with IllegalStateException as the expectation is that you only use initialized objects.

Btw. the builder pattern in general is a good way to avoid the issue with initialization, and in Kotlin you can even have an inline factory function that does the builder transparently (with lots of bits left out):

class TypeWithComplexInit private constructor(builder:Builder) {
    // code that uses the builder to initialize properties etc.

    class Builder(var propA:Int=0, var propB:Boolean=false)

    companion object {

        inline operator fun invoke(configurator: Builder.() -> Unit):TypeWithComplexInit {
            val b = Builder()
            b.configurator()
            return TypeWithComplexInit(b)
        }

    }
}
1 Like

I think it depends on the kind of contract, is it a verification of required arguments before computing on them, then you got an IllegalArgumentException in case there was a validation error.
If, however, the kind of computation was wrong, i.e. the states after it are not satisfying, then an IllegalStateException would be more appropriate.

It is somewhat true that illegal state exceptions shouldn’t exist as those can be, in theory, replaced by IllegalArgumentExceptions which check all possibilities for emerging illegal states.
But as it already sounds, this is practically unfortunate to impossible to hold on in productive software development even with fancy type systems.

1 Like

IMO, there’s not much difference between the two. When I think about exceptions, I first think about how they should be handled. For example, if an exception signals an unexpected external error, such as running out of disk space, pretty much the only thing you can do is to show an error message (perhaps not even log it, since there’s no free space). There are also expected exceptions that shouldn’t even exist in a well-designed programs, but sometimes you just have to do it. For example, a 3rd party library may throw an exception in a situation that is well expected in your software. In such cases, it makes sense to catch that exception as soon as possible and convert it into something more useful for flow control, such as a result-like object.

But when it comes down to things like IllegalStateException and IllegalArgumentException, they usually indicate a bug in software. It doesn’t really matter what kind of bug it is, as long as there are enough details in the error message. There are few exceptions to this rule, such as NumberFormatException, but that’s just bad library design.

Here’s an example that blurs the line between them even further. I designed a result-like class Outcome to avoid abusing exceptions and to have a neat way to organize code around expected errors. Sometimes, however, you know in advance that a certain call can’t possibly produce an exception (for example, Integer.parse(s) would never throw if you pass in a string that was previously matched against \d{3}). So I eventually came up with a test like this:

val result: Outcome<MyClass> = MyClass.parse(s)
val value: MyClass = resut.get()

Now, parse() can return either a failure or a success. And get() must throw an exception if called on a failure. What exception? Well, it’s an indication that the state of the result is wrong, so it must be an IllegalStateException, right? Seems so. But here’s another example:

fun tryParse(s: String): Outcome<MyClass> = // ...
fun parse(s: String): MyClass = tryParse(s).get()

Now I have two functions: one should be used when I don’t know in advance whether the argument is valid, the other one is to be called I do know that. But with an implementation like this, parse(s) will still throw an IllegalStateException on an invalid argument! This doesn’t make any sense because the Outcome instance is now purely an implementation detail of parse(). So, should I catch the exception in parse() and convert (or wrap) it into an IllegalArgumentException? One would think so, but it would just lead to unnecessary boilerplate while providing little to no value to the API user. Even if I were to encapsulate that boilerplate into something like getOrThrowIllegalArgumentException() and getOrThrowIllegalStateException() that would still be rather ugly.

Eventually I just decided to let IllegalStateException to propagate, and just never write catch (IllegalStateException) or catch (IllegalArgumentException).

2 Likes

+1

Also note, by catching ISE, you can easily make your coroutine/thread uncancellable, because CancellationException extends IllegalStateException

Since catching ISE/IAE is bad practice anyway, it doesn’t really matter which exception class you use.

If your function has an expected outcome, declare it as a separate exception type or use the enum / sealed class as the result.

2 Likes

Thanks for all the thoughtful answers! It seems that most of you favor the IllegalStateException. And it seems that it may have been a better design decision to not make a distinction between these two exception types in the first place.

1 Like

Now I’ll be a bit contrary to all of the other answers (while noting that this extends on a lot of the previous suggestions), but I do notice multiple points here. Let’s start by the exceptions, because IMO exceptions are almost always a bad design. Same goes with null. From your example I don’t know why s is nullable, if C is from something out of your control or if it’s a class you’re designing. So let’s dive into the first case.

You are in control of s and thus also C. Then my first question would be is why is c even nullable?

  • Nulls are still error-prone even in languages like Kotlin, although admitted a lot less so than in e.g. Java
  • The APIs for nullable types are inconsistent - most APIs use .map for monad/monad-like types, whereas nullable uses ?.let
  • Worst null still does not communicate, what does it mean? Not found? Optional? Etc.

In Kotlin nulls are usually extremely to avoid, while improving the communication. Even better the same design techniques should solve your Exception-issue. Depending on the use case the two most solutions in my experience are:

sealed class ProfilePicture {
    class NoImageSet: ProfilePicture()
    data class UrlProfilePicture(val url: String): ProfilePicture()
}

and when using it:

when (user.profilePicture) {
    is NoImageSet -> autogenerateAvatar()
    is UrlProfilePicture -> urser.profilePicture.url
}

That way you communicate the optional nature of things, and the reason behind it.

The second is where it’s some kind of expected error case, such as a domain error. What lacks here is a proper Either type that communicates what goes in inside the domain. Either as the name implies is similar to a pair, except only one value will be present at a time. So it is either Either.Right or Either.Left, but never both.

Fortunately it’s both easy to make yourself (Kotlin’s Missing Type, Either. Kotlin’s standard library extends… | by Robert Chrzanowski | Medium) or to just pull in from a framework (Λrrow Core).

That way you can communicate your intent much clearer (by convention the error is left):

interface LoginService {
    fun login(credentials: Credentials): Either<LoginError, User>
}

And usage

val user = loginService.login(credentials)

when (user) {
    is Either.Right -> showPage(user.b)
    is Either.Left -> when (user.a) {
        is LoginError.UserNotFound -> showUserNotFound(credentials.userName)
        is LoginError.WrongPassword -> showWrongPassword()
        is LoginError.Banned -> showUserIsBanned()
    }
}

In short designing new code with nullables is almost always (if not always, still haven’t seen a good use case) a bad design choice in new code. Nullable types are a necessary evil to deal with the mess we have from the legacy Java world and formats (e.g. json) that lacks the expression power to do better.

Also another note I make is that you’re using extension methods. Long story short, extension methods have a tendency to result in a poor design. Just like nullable types in my opinion a necessary evil for dealing with poor code/legacy code outside your control, so far the only good use case I’ve seen is dealing with legacy classes without a proper interface.

Otherwise things like decorator pattern, delegates, and similar designs (better structure too) provide much better alternatives.

Some of the issues I noted about extension methods:

  • They are harder to reason about
  • They are not located with the class they are extending making the code messier
  • If the code uses dependencies, your DI/IOC designs is likely to go out the window, or will need you to leak knowledge of the DI framework into your domain (yuck, your domain should only know about… yeah the domain)
  • It doesn’t take much complexity to make them hard to work with, and for instance refactoring the complexity out puts all sorts of issues of how to do so in a nice way (e.g. with encapsulation etc)
  • Even if decided to use with good reasons, I’d hesitate for pure human factors, a new developer does not know the considerations and may blindly imitate the design

I’ll probably make a more thorough post about these things later on, but basically my advice is, avoid nulls, Exceptions for expected errors - especially domain errors, and avoid extension methods. Instead there are usually better design-options out there

You are referring to a use case requiring to consider two branches while nullable operations referring to use cases where one branch of code is chosen when certain non-nullability conditions are met.

The functional equivalent of safe call operations is mapping functions over Option values yielding Option.None in case something was None/Null in the chain.

Meh, this isn’t even readable, why not Either.LoginSuccess->... and Either.LoginFailure?

Why?

They represent some neat sugar for functions expecting the instance of the class they extend as first argument allowing them to chain like normal class methods which is far more practical than simply composing static utility methods in Java.

Really?

Just to state my assumption, you are the functional Guy here?
I think strongly following the functional paradigm in a multi paradigm JVM language exhibiting many commonalities with Java isn’t the way to go for, but you can try it.
I think Scala suits better for the functional paradigm, they also don’t appreciate nullability that much and prefer Option<T> more than T?.

1 Like

Yes, and the other case right below it to which you also answer. One valid point that I did not focus that much on in my response is that Either does support nice (and consistent!) mapping, furthermore it does support flatMap which nullable types doesn’t trivially support.

If designing the Either yourself and for each case that is indeed a valid point :slight_smile: However, in practice I would use something like the Either from Arrow-KT or similar, which has to design for the same issue. So your question is a bit like asking why Pair is using first and second

On the readability I completely disagree, the only reason one would find that unreadable is habit. I taught that structure to my junior developers in my current project and they caught on to it in minutes, and one they had used it a few times it was completely natural - a lot more so than ?.let, ?: and all the other ? notations.

The true power does not come from Either.Right or Either.Left which indeed in a way just becomes another notation for the same deal. The power comes from the Left part containing an explicit value making it clear and communicative on what the missing value means. null means nothing apart from whatever implicit interpretation one puts over it, which almost always turn overloaded (in repositories it means not found, in models it’s optional/not set yet/etc.)

Here is a clear and meaningful communication that is easy to expand with multiple meanings, and due to the niceness of sealed class have a finite interpretations, thus no more exceptions for control flow (now exceptions are for the truly exceptional cases - as should be), no more forgotten exception handlers etc. Basically put your business logic in front.

Nullable types are nice for dealing with legacy frameworks, however, not matter how much you polish it, nulls are still a mistaken design that should not have been there. Now with the expressive power of Kotlin, it’s easy to actually design properly instead of using a structure with ambiguous and implicit meanings. Over and over we learn it’s better to consider programming a language rather than code, this observation have been made a multitude of times. Latest trend is in DDD, but it’s by far not the first time this observation has been made. So basically if you mean “not found”, well then tell me, instead of sending a null and expecting me to guess it (and please don’t use the JavaDoc argument, if I need to check that to understand your interface it’s too complicated).

I already provided a few reasons in my post. And I didn’t even mention testability, how sloppy of me :slight_smile: but be careful, I see a lot of embracing of extension functions lately - seems like every kid on the block wants to play with the new toy. I did try KTor, and my experiences were basically, it maked proper DI/IOC design cumbersome, I basically have to pass my DI around - the point of DI is that the objects do not know what they use. It makes slightly more complex structures harder to write, e.g., if I want a helper function it either has to be in a separate shared object (not sensible if I don’t want to share it - encapsulation) or it always have to be before usage, because there is no class etc. There are a lot of small details over and over again, to the point that I’m wondering if Spring for the endpoints is actually a better design for the endpoints, since it’s proper OOP, albeit with some nasty annotations/reflection logic.

Actually my original background is Java, but yes I do have strong a functional background too. Why are you stating that like it’s a bad thing? Keep in mind that the most major and game changing improvements over the latest years actually come from functional programming.

That is said my point of view on a lot of these things are shared by a lot of more pure Java/OOP people, the major difference is that some of my solutions are in part taken from the functional paradigm. If you read Clean Code, Effective Java, Domain Driven Design, Elegant Objects to mention a few, the same patterns of thinking will emerge. Exceptions should only be for exceptional cases, not domain logic, do communicate your domain, avoid nulls etc.

Both are bad IMO, Option<T> is marginally better, but so marginally that it’s close to pointless. Both designs suffer from the communication weaknesses, and the only benefits Option gives is a more consistent design, and that you are forced to consider if a null is an error or expected

1 Like

Nulls get used in a variety of subtly-different cases: when a value is not initialised, not available, not applicable, not known, secret, when a default should be assumed, as the result of a bug…⠀How does Left distinguish between those, and what sort of value would it hold to do so?

I’m guessing it depends on the context.⠀ Which would not make it significantly better than null.⠀(And arguably worse, because at least most of those meanings of ‘null’ are closely related to its everyday meaning, which ‘left’ certainly isn’t.)

I appreciate that an Either is simpler theoretically, needing no language support.⠀But Kotlin is not a minimalist language; it’s pragmatic (as illustrated by some of its syntactic sugar).⠀And I’d contend that in practice, nullability is used sufficiently often and differently from other constructs as to justify treating it differently.⠀Yes, it means a couple more operators, and complexity in the type system; but it makes for code that’s a lot clearer, shorter, and more intuitive to those experienced with it.⠀And being able to treat nullable types as supertypes of non-nullable ones is quite powerful.

(After all, if elegance and purity were all a programming language needed, then arguably there wouldn’t have been any functional languages after LISP…)

Many valuable ideas and techniques have spread from functional programming; but ‘most’?⠀You seem to be overstating the case a little.

And I’d argue that many functional ideas are copied a little too slavishly.⠀If I recall correctly, Martin Odersky, creator of Scala, pointed out that while much of the functional side of that language was new to many developers, he’d actually made very few developments there and most of the innovations were on the object-oriented side.

(For one unimportant but symbolic point of tradition, look at how the keyword for defining a function has begun with def all the way from LISP’s defun and Scheme’s define down to Scala’s def — where it never made any sense to me that you ‘define’ a function but not a property…⠀Kotlin, though, wasn’t afraid to break with tradition, and now fun sits well next to var and val for making clear what you’re defining.)

Eh?

Kotlin has no problem with top-level functions being declared earlier in the file that they’re used, or later, or in a separate file in the same package or a completely different package.

2 Likes

While very interseting this has nothing to do with the original question. Could we move the discussion about Null vs Either and the pros and cons of functional programming into it’s own topic?

4 Likes