Code style: absent-value in enums

I have an enum class like the following

enum class ErrorCause(val id: Short) {
  Electrical(1),
  Mechanical(2),
  Chemical(3);
}

However, the class that includes a field of type ErrorCause must not necessarily contain an error cause, so the value is effectively optional.

class Thing {
  var errorCause: ErrorCause? = null // to null or not to null, that is the question!
}

Would you prefer to add an enum value to express absence of an error cause like in this example:

enum class ErrorCause(val id: Short) {
  None(0),  // value to represent absence
  Electrical(1),
  Mechanical(2),
  Chemical(3);
}


 or would you use a nullable field instead? What are your reasons to prefer the one or other style?

1 Like

I would probably prefer the second version with a specific state for ErrorCause.None. null just doesn’t convey the same meaning. Is the error cause null because we don’t know what the cause is, or because it is not important, or because there is no error? null can have many meanings.

3 Likes

On the other hand, if the field is explicit about what null means, then you get to use all the language tools such as the Elvis operator to handle it more easily.

And I think you need to be explicit in both cases. Whether you use a null or a dummy value: does that value mean that there was no error? Or that there was an error but its cause is unknown? Or hasn’t been initialised yet? Or is known but doesn’t fit into any of the other values? Or you aren’t authorised to see it? Or that the action that might cause an error hasn’t happened yet? Or


(The ‘represent absence’ comment is hardly sufficient to explain any of those!)

So ISTM that a single dummy value has all the disadvantages of the null option.

(You could of course add multiple dummy values to distinguish those cases, but then you’d probably also want some way to distinguish the dummy values from the actual ones, and things get much more complicated.)

2 Likes

The case for a null value would be that you make the absence of an error explicit. The case for a None value would be that you don’t have to handle null values and can have consistent semantics. However, the semantics of “no error” do not actually match “an error”. Therefore I would go for a third solution:

sealed class Status {
    object Success: Status
    class Error(val errorCause: ErrorCause)
}

And you would add various methods to handle this. You could even treat Status as monad to handle errors automatically until you want to actually determine error status.

2 Likes

In cases like this, I tend to ask myself why I need the error code and error field, what is the desire of the user of the software that makes these things necessary? Usually, a user (or a programmer) does not want erroneous, faulty objects - too much mess downstream, and it is far too easy to use the object without checking the error code first.

I assume that this code would be used in a typical scenario of constructing an object from data from an external source (a file, a database, a website, user input, or whatever). Either this process works, or it fails. If the process works, you get a correct object. Or the process fails, and you get a hopefully informative error message. In the programming world, the function would not return a Thing, but a Result<Thing, Error> or an Either<Thing, Error>, and this object you can interrogate later to see if it contains a valid object or an error value.

For this purpose, Kotlin has a Result structure, but you can also homebrew something if you don’t want to require ErrorCause to become a Throwable:

enum class ErrorCause(val id: Short) {
    Electrical(1),
    Mechanical(2),
    Chemical(3);
}

class Thing(val phase: Int)

// derived from https://medium.com/@KaneCheshire/recreating-swifts-result-type-in-kotlin-f0a065fa6af1
sealed class Result<out S, out F>
data class Success<out S, out F>(val value: S) : Result<S, F>()
data class Failure<out S, out F>(val failure: F) : Result<S, F>()

fun result(achievedPhase: Int): Result<Thing, ErrorCause> {
    if (achievedPhase <= 1) return Failure(ErrorCause.Electrical)
    return when (achievedPhase) {
        2 -> Failure(ErrorCause.Mechanical)
        3 -> Failure(ErrorCause.Chemical)
        else -> Success(Thing(achievedPhase))
    }
}

fun processThingResult(cResult: Result<Thing, ErrorCause>) {
    println(
        when (cResult) {
            is Success -> "Thing in phase ${cResult.value.phase}"
            is Failure -> "Error because ${cResult.failure}"
        }
    )
}

fun main() {
    val coord1 = result(1)
    processThingResult(coord1)
    val coord2 = result(2)
    processThingResult(coord2)
    val coord3 = result(3)
    processThingResult(coord3)
    val coord4 = result(4)
    processThingResult(coord4)
}

Of course, the above code is rather elaborate (though a better Kotlin developer could possibly simplify it), but it would avoid the pain point of accidentally using a faulty object because someone forgot to check for the presence of an error code. A problem that becomes more and more likely if codebases grow and get older.

If in your case the real problem is that you need an object with error code (possibly to be cleaned based on the error code - still tricky, though, since someone may forget to clean the object, one reason why you generally want functions to return either a good object or nothing/an error), then the ‘Result’ method may be inappropriate, pdvrieze’s Status object would then be most ‘error-proof’, in my opinion.

Still, I think that the ‘best style’ depends very much on the result you want to achieve, the usage of the ErrorCode and Thing ‘downstream.’

1 Like

You should use null to represent an absent value. That’s what it’s for.

However


  • Using errorCause==null to indicate success is unclear. You shouldn’t do that.
  • You will almost certainly need also need an ErrorCause.UNKNOWN for unforseen errors, which does not mean the same thing as errorCause==null. null => There may be an error cause, but this variable doesn’t know it. UNKNOWN => The cause of this error couldnt’t be determined or doesn’t fit into one of the other categories that were known when the handler was written.

Yes, the ability to make some fields nullable and others non-nullable is one advantage of using a null to represent a missing value; a special enum value would force you to allow missing values everywhere (or, by its absence, nowhere).

Indeed — and again, using nulls means that that decision could apply to each field/parameter/variable individually, while using a special enum value would force you to use the same meaning everywhere.

So if you want to be able to allow a field to be ‘not initialised yet’ — a meaning which couldn’t apply to a function parameter — then you’d have to use null.⠀Similarly, if you wanted to allow a function parameter to mean ‘keep the current value’ — which wouldn’t make sense for some fields — then you’d have to use null there.