Why does EK9 require explicit constructors for uninitialised properties?

← Classes and OOP · Ref: Q104

EK9 requires that every property is either given a default value at declaration or initialised in a constructor. If a property is declared without a value, the compiler requires a constructor that sets it. This prevents null surprises.

THE RULE

Properties declared without values must be initialised in a constructor:

  Connection
    host as String?
    port as Integer?
    Connection()
      -> host as String, port as Integer
      this.host: host
      this.port: port

The '?' suffix means 'declared but not yet initialised'. Without '?' the compiler rejects the declaration immediately with E08180.

FLOW ANALYSIS ENFORCEMENT

EK9 uses flow analysis to verify that every property is initialised before use:

  E08180 — property not marked '?' and not initialised (must use '?' or provide a value)
  E08070 — property declared with '?' but never assigned in any constructor
  E08060 — property used before it has been initialised (would be NullPointerException in Java)

This eliminates the entire category of 'field not initialised' bugs at compile time.

WHY THIS MATTERS

In Java, fields default to null. You discover the problem at runtime with NullPointerException. In Python, missing attributes cause AttributeError at runtime. In Kotlin, lateinit crashes at runtime if accessed before init. EK9 catches ALL of these at compile time.

HOW TO SATISFY THE RULE

Option 1: Provide a default value at declaration:

  host <- "localhost"

Option 2: Use unset default (type constructor):

  host <- String()

Option 3: Declare without value and initialise in constructor:

  host as String?
  MyClass() -> host as String; this.host: host

Note: property initialisers must be simple — literals, type constructors, or literal collections only. Function calls and expressions are not allowed (E04050). Use a constructor for computed values.

See Q29 for unset variables. See Q94 for constructors. See Q93 for class basics. See Q632 for define-before-use patterns. See Q633 for initialisation across branches. See Q692 for field initialisation patterns.

Example

defines module qa.oop.uninitialised

  defines class

    Connection
      host as String?
      port as Integer?

      default private Connection()

      Connection()
        ->
          host as String
          port as Integer
        this.host: host
        this.port: port

      describe()
        <- rtn as String: `${host}:${port}`

      default operator ?

    DatabaseConfig
      host <- "localhost"
      port <- 5432
      database <- String()

      DatabaseConfig()
        -> database as String
        this.database: database

      describe()
        <- rtn as String: `${host}:${port}/${database}`

      default operator ?

  defines program

    UninitialisedDemo()
      stdout <- Stdout()

      // === UNINITIALISED PROPERTIES: must be set in constructor ===

      conn <- Connection("db.example.com", 3306)
      stdout.println(`Connection: ${conn.describe()}`)
      stdout.println(`IsSet: ${conn?}`)

      // === MIX: some defaults, some uninitialised ===

      db <- DatabaseConfig("myapp")
      stdout.println(`Database: ${db.describe()}`)

      // === Default values work without constructor args ===

      stdout.println(`DB isSet: ${db?}`)

Common mistakes

E08180 — Properties declared without '?' and without a default value must be initialised inline. The '?' suffix marks a property as explicitly uninitialised, requiring constructor assignment. If neither '?' nor a default is provided the compiler may report E08180. See ek9 -h E08180 for details.

Incorrect:

host as String

Correct:

host as String?

E08060 — If a constructor does not initialise all uninitialised properties, the compiler's flow analysis detects that 'port' is never assigned any value and reports E08060 — variable declared but never initialised. Every property must be set on every code path. See ek9 -h E08060 for details.

Incorrect:

this.host: host

Correct:

this.host: host
        this.port: port

E08060 — If the constructor forgets to initialise 'host' but a method later reads it, E08060 is triggered — is/may not be initialised before use. In Java this would be a NullPointerException at runtime. EK9 catches it at compile time through flow analysis. See ek9 -h E08060 for details.

Incorrect:

this.port: port

Correct:

this.host: host
        this.port: port

E50001 — Property initialisers must be simple values: a literal ('localhost'), a type constructor (String()), or a literal collection. Function calls and expressions are not allowed in property declarations. Java developers are used to 'private String host = computeDefault()' but EK9 requires E50001 — type must be a simple aggregate/list/dict. Use a constructor to compute values instead. See ek9 -h E50001 for details.

Incorrect:

host <- defaultHost()

Correct:

host <- "localhost"

E08180 — Properties declared without '?' and without a default value must be initialised inline. The '?' suffix marks a property as explicitly uninitialised, requiring constructor assignment. If neither '?' nor a default is provided, E08180 is triggered. See ek9 -h E08180 for details.

Incorrect:

host as String

Correct:

host as String?

E04050 — Property initialisers must be simple values: a literal ('localhost'), a type constructor (String()), or a literal collection. Function calls and expressions like String().trim() are not allowed in property declarations. Use a constructor for computed values. See ek9 -h E04050 for details.

Incorrect:

host <- String().trim()

Correct:

host <- "localhost"
Other ways to ask this
  • What happens if a class property has no default value in EK9?
  • How do I handle uninitialised fields in an EK9 class?
  • Why must I write a constructor when a property is not initialised?

Coming from another language?

Java: fields default to null/0/false, no compile-time enforcement of initialisation. Python: no field declarations, set in __init__, AttributeError if accessed before set. Rust: all fields must be initialised in struct literal, compiler enforces. Go: fields zero-valued by default (empty string, 0, nil). Kotlin: lateinit var for deferred initialisation, crashes at runtime if accessed too early. EK9: compile-time enforcement that properties are either declared with values or initialised in constructors, no null defaults.

Keywords: analysis, field, compile, NullPointerException, null, migrate, value, constructor, object-oriented, safety, default, initialise, uninitialized, flow, uninitialised, property, lateinit, required