Flexible Constructor Bodies in Practice

Validate arguments, compute values and initialise fields before calling super(...) or this(...), and avoid the classic null field seen by an overridden method.

SyntaxChild(String raw) { String clean = raw.strip(); // prologue: no use of this this.name = clean; // allowed: field without initialiser super(clean); // then the parent constructor }

Until Java 25, super(...) or this(...) had to be the very first statement in a constructor. Validation and argument preparation were pushed into static helper methods or done after the parent had already run. Flexible constructor bodies (JEP 513, final in Java 25) allow statements before that call.

The prologue and the epilogue

Code before super(...) or this(...) is the prologue. It may:

  • validate arguments and throw exceptions;
  • compute local variables and pass them to the call;
  • assign fields of the class being constructed, as long as they have no initialiser.

It may not read fields, call instance methods, or use this in any other way, because the object is not fully constructed yet. The compiler rejects those. Code after the call is the epilogue and works as constructors always have.

Why it matters in back-end code

Fail fast. A value object or entity that extends a base class can reject a bad email or a negative amount before the parent constructor does any work, such as registering the object or computing an ID.

No more null fields in overridden methods. If a parent constructor calls a method that a subclass overrides, the subclass method runs before the subclass constructor body. Its fields are still null or zero. Assigning those fields in the prologue fixes this at the source:

Child(String name) {
    this.name = name;   // assigned before Base() runs
    super();
}

Cleaner exceptions. Custom exceptions often build their message from several inputs. The message can now be computed in ordinary statements and then passed to super(message).

Records. A record's extra constructors must delegate to another constructor with this(...). Parsing input such as "10-20" into components can now happen in normal statements right before that call.

Mistakes people make

  • Trying to read a field or call a helper instance method in the prologue. Make the helper static, or move the work after super(...).
  • Assigning in the prologue a field that already has an initialiser such as = new ArrayList<>(). That is not allowed; drop the initialiser.
  • Seeing this as permission to call overridable methods from constructors. It makes the pattern safer, but a parent constructor calling subclass code is still fragile design.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Domain entities that extend a shared base class and must reject invalid input before the base assigns an ID or audit fields.
  • Framework-style handler classes whose base constructor registers routes by calling an overridden method.
  • Error types in an API layer that build a consistent message from status, error code and detail.

More examples

Fixing the null field seen by an overridden method

The parent constructor calls <code>route()</code> before the subclass constructor body would normally run. Assigning the field in the prologue means the override sees the real value.

Example · java
abstract class Handler {
    Handler() {
        IO.println("route " + route());
    }
    abstract String route();
}

class LateHandler extends Handler {
    private final String path;
    LateHandler(String path) {
        super();
        this.path = path;          // too late: route() already ran
    }
    String route() { return path; }
}

class EarlyHandler extends Handler {
    private final String path;
    EarlyHandler(String path) {
        this.path = path;          // prologue: assigned before Handler() runs
        super();
    }
    String route() { return path; }
}

void main() {
    new LateHandler("/orders");
    new EarlyHandler("/orders");
}
// Output:
// route null
// route /orders

Preparing arguments in a record constructor

The parsing statements run before <code>this(from, to)</code>, so no static <code>parse</code> helper is needed. The compact canonical constructor still does the range validation in one place.

Example · java
record PageRange(int from, int to) {
    PageRange {
        if (from < 1 || to < from) {
            throw new IllegalArgumentException("bad range " + from + "-" + to);
        }
    }

    // Parses "3-9" from a query string such as ?pages=3-9
    PageRange(String text) {
        String[] parts = text.strip().split("-", 2);
        if (parts.length != 2) {
            throw new IllegalArgumentException("expected from-to, got '" + text + "'");
        }
        int from = Integer.parseInt(parts[0]);
        int to = Integer.parseInt(parts[1]);
        this(from, to);
    }
}

void main() {
    IO.println(new PageRange("3-9"));
    for (String bad : List.of("7", "9-3")) {
        try {
            new PageRange(bad);
        } catch (IllegalArgumentException e) {
            IO.println(e.getMessage());
        }
    }
}
// Output:
// PageRange[from=3, to=9]
// expected from-to, got '7'
// bad range 9-3

Building an exception message before super

Validation and message formatting are plain statements in the prologue instead of a nested ternary inside <code>super(...)</code>. The field is still assigned after <code>super</code>, which is fine because nothing reads it during construction.

Example · java
class ApiException extends RuntimeException {
    private final int status;

    ApiException(int status, String code, String detail) {
        if (status < 400 || status > 599) {
            throw new IllegalArgumentException("not an error status: " + status);
        }
        String message = status + " " + code + (detail.isBlank() ? "" : ": " + detail);
        super(message);
        this.status = status;
    }

    int status() { return status; }
}

void main() {
    ApiException e = new ApiException(404, "order_not_found", "order 42 does not exist");
    IO.println(e.status() + " | " + e.getMessage());
    IO.println(new ApiException(409, "conflict", " ").getMessage());
    try {
        new ApiException(200, "ok", "");
    } catch (IllegalArgumentException bad) {
        IO.println(bad.getMessage());
    }
}
// Output:
// 404 | 404 order_not_found: order 42 does not exist
// 409 conflict
// not an error status: 200

Discussion

  • Be the first to comment on this lesson.