Skip to main content

Flexible Constructor Bodies in Java 25 (JEP 513): Validate Before super()

Java 25 finalizes JEP 513: statements before super() or this(). See the old error and workaround, validation and argument preparation, the parent-calls-child bug it fixes, and the compile errors that remain, with JDK 25 and 27 wordings compared.

Every Java developer eventually writes a constructor that wants to check something before it calls super(...): reject a negative salary, trim a string, parse one argument into two. And every Java developer before Java 25 got the same compiler error and worked around it with a static helper hidden inside the super(...) argument. JEP 513, final in Java 25, removes the rule that caused it. Ordinary statements may now come before super(...) or this(...), provided they do not touch the object that is still being built. This article shows the old error, the old workaround, what the new rule allows, the real bug it finally fixes (a parent constructor that calls a method the child overrides), and every compile error that is still there — including the fact that JDK 27 rewords most of them. Each claim comes from a real compile and run, and each code block links to its file in the companion repository.
Versions. Tested on JDK 25.0.4.1 (Temurin, LTS), JDK 27+35 (Temurin, GA 15 September 2026) and, for the “before” transcripts only, JDK 21. JEP 513 is final in 25: no --enable-preview. Compiling this code with --release 24 or lower is rejected (the exact message is in the section on what is still illegal), so a library that must still target 21 cannot use it yet. openjdk.org/jeps returned HTTP 403 to the tooling used for this article, so every behaviour below comes from javac and java themselves, not from the JEP text.

The rule Java had: super() first, and the workaround everyone wrote

A constructor in a subclass has to call a constructor of its superclass, because the parent part of the object must be built before the child part. Until Java 25 the language enforced that by demanding the call be the first statement. Here is a perfectly reasonable constructor that breaks the rule — validate the salary, tidy the name, then call the parent — compiled on JDK 21:
// Written with System.out so that JDK 21 can compile it: the ONLY thing wrong with it on 21 is the statement before super(...).
public class OnJdk21 {
    static class Person { Person(String n) {} }
    static class Employee extends Person {
        Employee(String name, int salary) {
            if (salary < 0) throw new IllegalArgumentException("negative");
            super(name.strip());
        }
    }
    public static void main(String[] args) {
        new Employee(" a ", 1);
        System.out.println("constructed");
    }
}
Source: OnJdk21.java. It is written with System.out so that JDK 21 can compile it; the only thing wrong with it on 21 is the if before super(...).
$ javac broken/OnJdk21.java && java OnJdk21     (OpenJDK Runtime Environment (build 21.0.10+7-Ubuntu-124.04))
broken/OnJdk21.java:7: error: call to super must be first statement in constructor
            super(name.strip());
                 ^
1 error
Output: 03-same-file-on-21-25-27.txt. The message, “call to super must be first statement in constructor”, is short and completely clear. The trouble is what you were supposed to do next. The idiom was to move the checks into a static method and call that inside the super(...) argument, because an argument expression is allowed to run code:
    static class Employee extends Person {
        final int salary;

        Employee(String name, int salary) {
            super(checkAndClean(name, salary));   // validation smuggled into an argument expression
            this.salary = salary;
        }

        private static String checkAndClean(String name, int salary) {
            if (salary < 0) throw new IllegalArgumentException("salary must be >= 0, got " + salary);
            return name.strip();
        }
    }
Source: BeforeJava25.java. It works — the same file compiles and runs on JDK 21 and on 25, as the transcript below shows — but the validation has moved out of the constructor and into a helper whose only job is to run before super, and the helper has to return the single value the parent needs. The moment you want to pass two values up, computed from one input, this pattern stops being pleasant.
$ javac src/BeforeJava25.java && java BeforeJava25     (OpenJDK Runtime Environment (build 21.0.10+7-Ubuntu-124.04))
  Person constructor ran for 'Asha'
  name='Asha' salary=50000
  caught: salary must be >= 0, got -1
Output: 02-the-old-workaround.txt. That transcript is the JDK 21 run of the workaround; the JDK 25 run of the same file is in the same file.
Before Java 25 (checked on JDK 21) super(…) the rest of the constructor: this is usable Java 25 (JEP 513) prologue: validate, parse, compute. No use of this. super(…) the rest: this is usable The amber part is the new territory. Everything after super(…) behaves exactly as it always did.
The top bar is the old shape and the bottom bar the new one; the only thing that changed is the amber segment. That is worth holding onto, because the next sections are all about what is safe to put in it and why the compiler is strict about what is not. In this article I will call it the prologue: the statements before super(...) or this(...).

Going deeper on this section

Validate before the parent even exists

With the new rule the constructor reads in the order you would say it out loud: check the input, clean it up, call the parent, then assign the child’s own field.
    static class Employee extends Person {
        final int salary;

        Employee(String name, int salary) {
            // Validate and tidy first. If this throws, Person's constructor never runs.
            if (salary < 0) throw new IllegalArgumentException("salary must be >= 0, got " + salary);
            String cleaned = name.strip();
            super(cleaned);
            this.salary = salary;
        }
    }
Source: ValidateBeforeSuper.java. Note the two statements before super(cleaned): an if ... throw and a local variable. Run it with a valid and an invalid salary:
$ javac src/ValidateBeforeSuper.java && java ValidateBeforeSuper     (OpenJDK Runtime Environment Temurin-25.0.4.1+1 (build 25.0.4.1+1-LTS))
valid input:
  Person constructor ran for 'Asha'
  name='Asha' salary=50000
invalid input:
  caught: salary must be >= 0, got -1  (note: no 'Person constructor ran' line above)
Output: 01-validate-before-super.txt, and the JDK 27 run of the same file is in the same transcript. The valid case prints the parent’s line, because Person was constructed. The invalid case prints no such line: the exception left the constructor before Person’s constructor ever started. That is the point of validating first. With the old shape the parent constructor would already have run (and might have registered the half-built object somewhere, or opened a resource) before the check could fail.
The one idea to carry forward. Until super(...) returns, the object exists but is only half built: the parent’s fields have not been set and the child’s fields hold their defaults (null, 0, false). Everything the compiler forbids in the prologue is a way of reading or leaking that half-built object. The section on the parent-calls-child bug below shows what happens when the rule is not there to stop you.
Going deeper: what is allowed in the prologue, in one list

From the compiles in this article: local variables, if, throw, try/catch, loops, calls to static methods and to methods on values you were passed, and plain assignment to a field of this class that has no initializer. What is not allowed is anything that names this implicitly or explicitly: reading a field, calling an instance method, creating an inner-class instance, capturing an instance field in a lambda, passing this anywhere. The full list of rejections, with the compiler’s wording on 25 and 27, is in the section on what is still illegal.

Going deeper on this section

Compute the arguments you pass up

Validation is the small use. The larger one is preparing the arguments. A Meeting that is a kind of Time is built from a string like "9:05", but Time wants two integers. Before Java 25 that needed either a static factory method or a private constructor chain. Now it is straight-line code:
    static class Meeting extends Time {
        final String title;
        Meeting(String title, String at) {
            String[] parts = at.split(":");
            int h = Integer.parseInt(parts[0]);
            int m = Integer.parseInt(parts[1]);
            super(h, m);
            this.title = title;
        }
    }
Source: ParseThenSuper.java, where Time is a plain class whose constructor takes the hours and minutes. Run:
$ javac src/ParseThenSuper.java && java ParseThenSuper     (OpenJDK Runtime Environment Temurin-25.0.4.1+1 (build 25.0.4.1+1-LTS))
design review at 09:05
bad input failed before Time was built: NumberFormatException
Output: 05-prologue-in-practice.txt. A malformed time fails with a NumberFormatException before Time is constructed, same as the validation case. The prologue is ordinary code, so try/catch works too, which lets a constructor fall back to a default instead of failing:
    static class Server extends Port {
        Server(String configured) {
            int parsed;
            try {
                parsed = Integer.parseInt(configured);
            } catch (NumberFormatException e) {
                parsed = 8080;                 // fall back instead of failing
            }
            super(parsed);
        }
    }
Source: TryInPrologue.java.
$ javac src/TryInPrologue.java && java TryInPrologue     (OpenJDK Runtime Environment Temurin-25.0.4.1+1 (build 25.0.4.1+1-LTS))
configured '9090'  -> port 9090
configured 'oops'  -> port 8080
Output: 05-prologue-in-practice.txt. And the rule is not special to super: a constructor that delegates with this(...) gets the same freedom.
    ThisChaining(String id) {
        String normalised = id.trim().toLowerCase();
        int defaultRetries = normalised.startsWith("critical") ? 5 : 1;
        this(normalised, defaultRetries);
    }

    ThisChaining(String id, int retries) {
        this.id = id;
        this.retries = retries;
    }
Source: ThisChaining.java.
$ javac src/ThisChaining.java && java ThisChaining     (OpenJDK Runtime Environment Temurin-25.0.4.1+1 (build 25.0.4.1+1-LTS))
critical-payment retries=5
report-export retries=1
Output: 05-prologue-in-practice.txt. One constructor normalises the id and picks a default retry count, then hands both to the real constructor, with no helper method and no repeated logic.

Going deeper on this section

The bug this actually fixes: a parent constructor that calls the child

Here is the half-built-object idea from earlier cashing in. Suppose the parent’s constructor calls a method, and the child overrides that method to read one of the child’s own fields. In Java the parent constructor runs first, so the overridden method runs while the child’s field is still at its default. That is the classic trap:
    static class Base {
        Base() {
            IO.println("  Base constructor calls describe(): " + describe());
        }
        String describe() { return "base"; }
    }

    // The bug: describe() runs from Base's constructor, before Broken's own field has been assigned.
    static class Broken extends Base {
        private final String label;
        Broken(String label) {
            super();
            this.label = label;
        }
        @Override String describe() { return "label=" + label; }
    }

    // The fix: assign the field in the prologue, before super() runs Base's constructor.
    static class Fixed extends Base {
        private final String label;
        Fixed(String label) {
            this.label = label;   // allowed in the prologue: a plain assignment to a field of THIS class
            super();
        }
        @Override String describe() { return "label=" + label; }
    }
Source: OverridableCall.java. Broken assigns its field after super(), so by the time the parent’s constructor calls describe(), the field has not been set. Fixed assigns it before super(), in the prologue, which is now legal because it is a plain assignment to a field of this class with no initializer.
$ javac src/OverridableCall.java && java OverridableCall     (OpenJDK Runtime Environment Temurin-25.0.4.1+1 (build 25.0.4.1+1-LTS))
Broken (assign after super()):
  Base constructor calls describe(): label=null
Fixed (assign before super()):
  Base constructor calls describe(): label=hello
Output: 04-overridable-call-bug-and-fix.txt. The first run prints label=null, from a field that is final and was going to be given a value one line later. The second prints label=hello.
Broken: assign after super() Base constructor runs describe() sees label = null this.label = “hello” Fixed: assign in the prologue this.label = “hello” Base constructor runs describe() sees label = hello The bug is the order. JEP 513 lets a constructor choose the order for fields it owns.
The top timeline is the bug, the bottom one is the fix, and the argument is only the order of the same three boxes. That is what the picture is for: nothing about the parent changed.
Do not build a design on this. Assigning fields before super() repairs a symptom, but the cause is that the parent constructor calls an overridable method. If you own the parent, stop doing that (make the method private or final, or move the call out of the constructor). Use the prologue fix when the parent is someone else’s and you cannot change it.

Going deeper on this section

What is still illegal, and how JDK 25 and 27 word it

The prologue is not a free-for-all. The compiler still refuses anything that would read or leak the half-built object. Every one of these fails to compile on both JDKs, and I ran each on both because the wording is not the same. Compare the two runs in the block below:
$ javac broken/ReadFieldBeforeSuper.java     (25.0.4.1+1)
ReadFieldBeforeSuper.java:5: error: cannot reference size before supertype constructor has been called
1 error
$ javac broken/ReadFieldBeforeSuper.java     (27+35)
ReadFieldBeforeSuper.java:5: error: reference to size may only appear after an explicit constructor invocation
1 error
Output: 07-still-illegal-25-vs-27.txt, from ReadFieldBeforeSuper.java. JDK 25 says cannot reference size before supertype constructor has been called. JDK 27 says reference to size may only appear after an explicit constructor invocation. The rule is identical; the sentence changed, across the field-reading, method-calling, this-passing, lambda-capturing and initialised-field cases in that file. Three messages did not change: 'return' not allowed before explicit constructor invocation, redundant explicit constructor invocation and explicit constructor invocation not allowed here.
You wrote before super(...)SourceWhy it is rejected
Read a field of this classReadFieldBeforeSuper.javaFields are still at defaults
Call an instance methodCallMethodBeforeSuper.javaThe method could read any field
Pass or store thisPassThisBeforeSuper.javaThe half-built object would escape
Capture an instance field in a lambdaInnerOuterThis.javaThe lambda holds this
Create an inner-class instanceNewInnerBeforeSuper.javaIt needs an enclosing this
Assign a field that has an initializerAssignFieldWithInitializer.javaThe initializer would run afterwards and overwrite it
Assign a field the parent declaresAssignInheritedField.javaThe parent has not built its part yet
return before the callReturnBeforeSuper.javaThe parent would never be constructed
super(...) in an if/elseSuperInBranch.javaIt must appear once, at the top level of the body
A second explicit constructor callTwoSuperCalls.javaRedundant
Every row’s exact message on both JDKs is in 07-still-illegal-25-vs-27.txt. The third column is my reading of why the rule exists; the compiler itself only says what is not allowed.
Wording changed between 25 and 27, so do not match on it. If a build script, an IDE quick-fix or a linter greps compiler output for “before supertype constructor has been called”, it will stop matching on JDK 27. The check itself did not get looser or stricter in any case I tried; only the sentence changed. I did not find this in the release notes, so treat it as observed.
Going deeper: what javac actually emits, and the --release trap

The rule that stood in the way was the language’s, and the JVM runs the result without complaint. Compile a constructor with a statement before the call and read the bytecode:

$ javap -c -p 'PrologueBytecode$Child'     (OpenJDK Runtime Environment Temurin-25.0.4.1+1 (build 25.0.4.1+1-LTS))
class PrologueBytecode$Child extends PrologueBytecode$Parent {
  PrologueBytecode$Child(int);
    Code:
         0: iload_1
         1: iconst_0
         2: invokestatic  #1                  // Method java/lang/Math.max:(II)I
         5: istore_2
         6: aload_0
         7: iload_2
         8: invokespecial #7                  // Method PrologueBytecode$Parent."<init>":(I)V
        11: return
}

From PrologueBytecode.java, output in 06-bytecode.txt. Offsets 0 to 5 are the Math.max call and a store into a local variable; the invokespecial of the parent constructor is at offset 8. The class runs (the demo prints ran when executed), so the JVM accepted code before the constructor call. What Java 25 changed is that javac stops rejecting it, while still enforcing the rules in the table.

The language level matters, and this is the trap for libraries. Compiling the same file for an older target from a JDK 25 compiler fails, and the message tells you the fix:

$ javac --release 21 broken/OnJdk21.java     (25.0.4.1+1)
OnJdk21.java:7: error: flexible constructors is not supported in -source 21
            super(name.strip());
                 ^
  (use -source 25 or higher to enable flexible constructors)
1 error
exit=1

From OnJdk21.java, output in 08-release-levels.txt, which also shows --release 24 failing the same way and --release 25 compiling cleanly (exit=0). A project that builds with --release 21 for compatibility cannot use flexible constructor bodies until it raises that number.

Going deeper on this section

Should you use this?

Yes, for the two things it is good at: failing fast before the parent exists, and computing the arguments you pass up without a throwaway static method. Both make constructors read in the order the logic runs, and neither needs anything but a JDK 25 compiler. Be cautious about two things. First, the field-assignment trick is a workaround for a parent that misbehaves, not a pattern to design toward. Second, the constructor is still the wrong place for anything elaborate: if validating an object takes twenty lines, a static factory method with a private constructor gives you a name for what is being checked and a place to return something other than an exception.
Records and enums were not tested here. This article only ran ordinary classes. A record’s canonical constructor has its own validation mechanism (the compact constructor), and I did not check how the new rule interacts with it, so nothing above should be read as a claim about records or enums.

Further reading

No Comments yet!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.