[Canonical page](<https://docs.dhtmlx.com/planning-engine/java/error-handling/>)
[Full documentation index](<https://docs.dhtmlx.com/planning-engine/llms.txt>)


# Error handling

This guide covers how Planning Engine reports failures in Java: which problems throw, which come back as data inside a normal result, and how to get a live project working again after each kind.

## How failures surface

Planning Engine reports failures with unchecked exceptions. No method declares `throws`, so the compiler won't remind you to catch anything. Every call is synchronous, so the call itself throws the failure. There's no `CompletableFuture` to inspect.

Every engine exception extends [`PlenException`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/plen-exception.md>), an abstract subclass of `RuntimeException`. It has two concrete subclasses:

| Exception | Meaning | Project state after the exception |
|---|---|---|
| [`InputException`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/input-exception.md>) | The data or options you passed are invalid. | Unchanged. The project keeps the previous accepted state and its cached result. |
| [`ProjectException`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/project-exception.md>) | The input may be fine, but the call doesn't fit the project's current state. | Unchanged. You fix it by calling the right method first. |

The public [`code`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/project-exception.md#code>) field of a `ProjectException` is a [`ProjectError`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/project-exception.md#projecterror>) value:

| Code | `getMessage()` | When it occurs |
|---|---|---|
| `NO_BASELINE` | `no-baseline` | [`preview`](<https://docs.dhtmlx.com/planning-engine/java/api/project-preview.md>), [`loadProfile`](<https://docs.dhtmlx.com/planning-engine/java/api/project-load-profile.md>), [`evaluate`](<https://docs.dhtmlx.com/planning-engine/java/api/project-evaluate.md>), [`suggestAssignments`](<https://docs.dhtmlx.com/planning-engine/java/api/project-suggest-assignments.md>) or [`roster`](<https://docs.dhtmlx.com/planning-engine/java/api/project-roster.md>) on a project that has no solved baseline, including right after `apply` |
| `STALE_BASELINE` | `stale-baseline` | A `baselineId` in the options or in [`SchedulingChanges`](<https://docs.dhtmlx.com/planning-engine/java/api/changes/scheduling-changes.md>) doesn't name the current baseline |
| `NEEDS_RESYNC` | `needs-resync` | [`solve(null, …)`](<https://docs.dhtmlx.com/planning-engine/java/api/project-solve.md>), `apply` or [`level`](<https://docs.dhtmlx.com/planning-engine/java/api/project-level.md>) after an `apply` rejected an operation, until a full `solve(data, …)` succeeds |

Most of what goes wrong in a plan isn't an exception at all. Dependency cycles, constraint conflicts, dropped links, a planner that didn't converge and rejected [`apply`](<https://docs.dhtmlx.com/planning-engine/java/api/project-apply.md>) operations come back inside the result. An exception means the call itself couldn't run.

Two other parts of the library have their own exception types. [`SkillLayer.slots`](<https://docs.dhtmlx.com/planning-engine/java/api/skill-layer-slots.md>) throws [`MatchException`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/match-exception.md>), which isn't a `PlenException`. The JSON API returns error envelopes instead of throwing. A few JDK exceptions, mostly `NullPointerException`, cover misuse of the types themselves. They're listed under [JDK exceptions](<https://docs.dhtmlx.com/planning-engine/java/error-handling.md#jdk-exceptions>).

## Catching errors

`catch (PlenException e)` covers every engine error. Catch the subclasses when you need to tell them apart:

```java
import org.dhtmlx.plen.*;

import java.util.List;

public class CatchErrors {
    public static void main(String[] args) throws Exception {
        var data = new ProjectData().withTasks(List.of(
            new TaskData(Id.num(1))
                .withStartDate(Moment.fromYmdHm(2026, 1, 5, 8, 0))
                .withDuration(2.0)));

        try {
            ScheduleResult result = Engine.schedule(data, new SolveOptions());
            System.out.println(result.updates.size() + " tasks moved");
        } catch (InputException e) {
            System.err.println("bad input at " + e.path + ": " + e.getMessage());
        } catch (ProjectException e) {
            System.err.println("wrong project state: " + e.code);
        }
    }
}
```

`getMessage()` holds different things on the two subclasses:

- On an [`InputException`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/input-exception.md>) it's only the description, such as `invalid duration`. The name of the bad field is in the public [`path`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/input-exception.md#path>) field, so read both when you show the error to a user.
- On a [`ProjectException`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/project-exception.md>) it's the kebab-case code (`no-baseline`, `stale-baseline`, `needs-resync`). Branch on [`code`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/project-exception.md#code>), not on the text.

## Input errors

An [`InputException`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/input-exception.md>) carries:

- [`kind`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/input-exception.md#kind>), an [`InputErrorKind`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/input-exception.md#inputerrorkind>). Calls on the typed API produce `VALIDATION`. `PARSE` comes only from JSON decoding: [`Decode`](<https://docs.dhtmlx.com/planning-engine/java/api/json/decode.md>), [`Wire`](<https://docs.dhtmlx.com/planning-engine/java/api/json/wire.md>) and [`JsonProject`](<https://docs.dhtmlx.com/planning-engine/java/api/json/json-project.md>).
- `getMessage()`, a short description such as `invalid duration`.
- [`path`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/input-exception.md#path>), the offending field in its JSON (wire) spelling: `tasks[0].duration`, `assignments[1].id`, `calendars[0].worktime.days`, `scope.resources`. It's `null` for parse errors and for refusals that concern the whole state, such as [`suggestAssignments`](<https://docs.dhtmlx.com/planning-engine/java/api/project-suggest-assignments.md>) on a cyclic project.

Paths use the snake_case wire names, not the Java field names. [`TaskData.constraintDate`](<https://docs.dhtmlx.com/planning-engine/java/api/data/task-data.md>) on the fourth task is reported as `tasks[3].constraint_date`.

With the default [`Validation.STRICT`](<https://docs.dhtmlx.com/planning-engine/java/api/project/validation.md>), [`solve(data, …)`](<https://docs.dhtmlx.com/planning-engine/java/api/project-solve.md>) and [`Engine.schedule`](<https://docs.dhtmlx.com/planning-engine/java/api/engine-schedule.md>) reject the input on the first problem they find. Strict validation rejects, among other things:

- a leaf task with fewer than two of `startDate`, `endDate` and `duration`, or with all three when they don't agree
- a dated constraint (`SNET`, `FNLT`, ...) without a `constraintDate`
- duplicate task, link or calendar ids (`Id.num(1)` and `Id.str("1")` count as the same id)
- a cycle in the `parent` hierarchy
- a summary with `startDate` + `duration` (a summary takes both dates or neither)
- a negative `duration`, or a `priority` outside 0..1000
- a [`Worktime.days`](<https://docs.dhtmlx.com/planning-engine/java/api/data/worktime.md#days>) array that doesn't have exactly 7 entries, and calendars with no working time
- `maxIterations = 0`, a non-finite lag, or a negative or non-finite `defaultResourceCapacity`

The full list per field is on the reference pages for [`ProjectData`](<https://docs.dhtmlx.com/planning-engine/java/api/data/project-data.md>), [`TaskData`](<https://docs.dhtmlx.com/planning-engine/java/api/data/task-data.md>) and [`AssignmentData`](<https://docs.dhtmlx.com/planning-engine/java/api/data/assignment-data.md>).

### Reporting a validation error with its path

```java
import org.dhtmlx.plen.*;

import java.util.List;

public class ReportPath {
    public static void main(String[] args) throws Exception {
        var data = new ProjectData().withTasks(List.of(
            new TaskData(Id.num(1))
                .withStartDate(Moment.fromYmdHm(2026, 1, 5, 8, 0))
                .withDuration(-1.0)));

        try {
            ScheduleResult result = Engine.schedule(data, new SolveOptions());
            System.out.println(result.updates.size() + " tasks moved");
        } catch (InputException e) {
            // Prints "tasks[0].duration: invalid duration"
            String path = e.path != null ? e.path : "<input>";
            System.err.println(path + ": " + e.getMessage());
        }
    }
}
```

### A rejected replacement keeps the old state

On a live [`Project`](<https://docs.dhtmlx.com/planning-engine/java/api/project/project.md>), a [`solve(data, …)`](<https://docs.dhtmlx.com/planning-engine/java/api/project-solve.md>) that fails validation installs nothing. The project keeps its accepted baseline, and `solve(null, …)` still returns the result you had before:

```java
import org.dhtmlx.plen.*;

import java.util.List;

public class RejectedReplacement {
    static TaskData leaf(int id, double duration) {
        return new TaskData(Id.num(id))
            .withStartDate(Moment.fromYmdHm(2026, 1, 5, 8, 0))
            .withDuration(duration);
    }

    public static void main(String[] args) throws Exception {
        var options = new SolveOptions();
        var project = new Project();
        ScheduleResult first = project.solve(new ProjectData().withTasks(List.of(leaf(1, 2.0))), options);

        try {
            project.solve(new ProjectData().withTasks(List.of(leaf(2, -1.0))), options);
        } catch (InputException e) {
            System.err.println("replacement rejected at " + e.path + ": " + e.getMessage());
        }

        // Still the first data set.
        ScheduleResult again = project.solve(null, options);
        System.out.println(again.equals(first)); // true
    }
}
```

## Project errors

A [`ProjectException`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/project-exception.md>) tells you the call came at the wrong moment. Each code has one recovery:

| Code | Recovery |
|---|---|
| `NO_BASELINE` | Call `solve` first. After [`apply`](<https://docs.dhtmlx.com/planning-engine/java/api/project-apply.md>), call `solve(null, …)` before [`preview`](<https://docs.dhtmlx.com/planning-engine/java/api/project-preview.md>), [`loadProfile`](<https://docs.dhtmlx.com/planning-engine/java/api/project-load-profile.md>), [`evaluate`](<https://docs.dhtmlx.com/planning-engine/java/api/project-evaluate.md>) or [`suggestAssignments`](<https://docs.dhtmlx.com/planning-engine/java/api/project-suggest-assignments.md>). |
| `STALE_BASELINE` | The project was solved, edited or leveled since you read the token. Take [`baselineId`](<https://docs.dhtmlx.com/planning-engine/java/api/results/schedule-result.md#baselineid>) from the latest result and retry. |
| `NEEDS_RESYNC` | Send the full data again with [`solve(data, …)`](<https://docs.dhtmlx.com/planning-engine/java/api/project-solve.md>). Nothing else is accepted until that call succeeds. |

### Telling the codes apart

`ProjectError` is an enum, so a `switch` expression on `e.code` covers every code without a `default` branch:

```java
import org.dhtmlx.plen.*;

public class TellCodesApart {
    static String previewOrExplain(Project project, SchedulingChanges changes, String baselineId) {
        try {
            ScheduleResult result = project.preview(changes, new PreviewOptions().withBaselineId(baselineId));
            return result.updates.size() + " tasks would move";
        } catch (ProjectException e) {
            return switch (e.code) {
                case NO_BASELINE -> "solve the project first";
                case STALE_BASELINE -> "the plan changed since you loaded it; refresh and try again";
                case NEEDS_RESYNC -> "reload the full project";
            };
        } catch (InputException e) {
            return "invalid change at " + e.path + ": " + e.getMessage();
        }
    }

    public static void main(String[] args) throws Exception {
        // An unsolved project: prints "solve the project first".
        System.out.println(previewOrExplain(new Project(), new SchedulingChanges(), null));
    }
}
```

[`preview`](<https://docs.dhtmlx.com/planning-engine/java/api/project-preview.md>) checks both [`options.baselineId`](<https://docs.dhtmlx.com/planning-engine/java/api/project/preview-options.md#baselineid>) and [`changes.baselineId`](<https://docs.dhtmlx.com/planning-engine/java/api/changes/scheduling-changes.md#baselineid>). Baseline tokens are opaque and unique to the process, so never store them across processes or compare them in tests.

### Recovering from `NEEDS_RESYNC`

A rejected operation in [`apply`](<https://docs.dhtmlx.com/planning-engine/java/api/project-apply.md>) doesn't throw. It's listed in [`ApplyReport.rejected`](<https://docs.dhtmlx.com/planning-engine/java/api/changes/apply-report.md#rejected>) and sets [`needsResync`](<https://docs.dhtmlx.com/planning-engine/java/api/changes/apply-report.md#needsresync>). From then on, `solve(null, …)`, `apply` and [`level`](<https://docs.dhtmlx.com/planning-engine/java/api/project-level.md>) throw a [`ProjectException`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/project-exception.md>) with `code == ProjectError.NEEDS_RESYNC`, because your model and the project's model no longer agree. Resync with the full data you hold:

```java
import org.dhtmlx.plen.*;

import java.util.List;

public class Resync {
    static ScheduleResult commit(Project project, SchedulingChanges changes, ProjectData sourceOfTruth) {
        var options = new SolveOptions();
        ApplyReport report = project.apply(changes);
        if (report.needsResync) {
            for (RejectedOperation rejected : report.rejected) {
                System.err.println("rejected " + rejected.path + ": " + rejected.message);
            }
            return project.solve(sourceOfTruth, options);
        }
        return project.solve(null, options);
    }

    public static void main(String[] args) throws Exception {
        var data = new ProjectData().withTasks(List.of(
            new TaskData(Id.num(1))
                .withStartDate(Moment.fromYmdHm(2026, 1, 5, 8, 0))
                .withDuration(2.0)));
        var project = new Project();
        project.solve(data, new SolveOptions());

        // Task 999 doesn't exist: the operation is rejected.
        var changes = new SchedulingChanges();
        changes.tasks.update.add(new TaskUpdate(Id.num(999)).withDuration(Patch.of(3.0)));

        ScheduleResult result = commit(project, changes, data);
        System.out.println("resynced, baseline " + result.baselineId);
    }
}
```

`solve` deep-copies the data it receives, so you can pass the same [`ProjectData`](<https://docs.dhtmlx.com/planning-engine/java/api/data/project-data.md>) object again. If the resync data itself fails validation, the [`InputException`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/input-exception.md>) leaves the project in the `NEEDS_RESYNC` state. Fix the data and call [`solve(data, …)`](<https://docs.dhtmlx.com/planning-engine/java/api/project-solve.md>) again. The [recovery how-to](<https://docs.dhtmlx.com/planning-engine/java/how-to/recover-from-rejected-changes.md>) covers this flow in an editor.

## Results that are not errors

These all come back in a normal return value. Check them on every result you show to a user:

| Where | What it means |
|---|---|
| [`ScheduleResult.cycles`](<https://docs.dhtmlx.com/planning-engine/java/api/results/schedule-result.md#cycles>) | Dependency cycles. A cyclic solve returns unchanged dates and empty `updates`, and still installs a baseline. |
| [`ScheduleResult.conflicts`](<https://docs.dhtmlx.com/planning-engine/java/api/results/schedule-result.md#conflicts>) | [`Conflict.ConstraintViolation`](<https://docs.dhtmlx.com/planning-engine/java/api/results/conflict.md#constraintviolation>) (a link and a hard constraint disagree, the constraint wins) and [`Conflict.CalendarNonWorking`](<https://docs.dhtmlx.com/planning-engine/java/api/results/conflict.md#calendarnonworking>) |
| [`ScheduleResult.notes`](<https://docs.dhtmlx.com/planning-engine/java/api/results/schedule-result.md#notes>) | Input the engine dropped ([`Note.DroppedLink`](<https://docs.dhtmlx.com/planning-engine/java/api/results/note.md#droppedlink>), [`Note.DroppedTask`](<https://docs.dhtmlx.com/planning-engine/java/api/results/note.md#droppedtask>), [`Note.DroppedAssignment`](<https://docs.dhtmlx.com/planning-engine/java/api/results/note.md#droppedassignment>)) or fixed ([`Note.Repaired`](<https://docs.dhtmlx.com/planning-engine/java/api/results/note.md#repaired>)) |
| [`ScheduleResult.converged`](<https://docs.dhtmlx.com/planning-engine/java/api/results/schedule-result.md#converged>) | `false` when the planner didn't reach a fixed point within [`config.maxIterations`](<https://docs.dhtmlx.com/planning-engine/java/api/data/scheduling-config.md#maxiterations>). The dates are still valid output. |
| [`ApplyReport.rejected`](<https://docs.dhtmlx.com/planning-engine/java/api/changes/apply-report.md#rejected>) | Operations that [`apply`](<https://docs.dhtmlx.com/planning-engine/java/api/project-apply.md>) refused. The other operations in the batch were still applied. |
| [`CandidateResult.error`](<https://docs.dhtmlx.com/planning-engine/java/api/evaluate/candidate-result.md>) | An [`evaluate`](<https://docs.dhtmlx.com/planning-engine/java/api/project-evaluate.md>) candidate whose change set was rejected. That candidate has no metrics. The other candidates are scored normally. |

### Checking a schedule for problems

[`Conflict`](<https://docs.dhtmlx.com/planning-engine/java/api/results/conflict.md>) and [`Note`](<https://docs.dhtmlx.com/planning-engine/java/api/results/note.md>) are abstract classes with a closed set of nested subclasses, so read them with `instanceof` patterns:

```java
import org.dhtmlx.plen.*;

import java.util.List;

public class CheckProblems {
    static LinkData link(String id, String source, String target) {
        return new LinkData().withId(Id.str(id)).withSource(Id.str(source)).withTarget(Id.str(target));
    }

    public static void main(String[] args) throws Exception {
        Moment start = Moment.fromYmdHm(2026, 1, 5, 8, 0);
        var data = new ProjectData()
            .withTasks(List.of(
                new TaskData(Id.str("a")).withStartDate(start).withDuration(1.0),
                new TaskData(Id.str("b")).withStartDate(start).withDuration(1.0)))
            // a -> b -> a: a dependency cycle, reported as data.
            .withLinks(List.of(link("l1", "a", "b"), link("l2", "b", "a")));

        ScheduleResult result = Engine.schedule(data, new SolveOptions());
        for (Cycle cycle : result.cycles) {
            System.out.println("cycle through tasks " + cycle.tasks);
        }
        for (Conflict conflict : result.conflicts) {
            if (conflict instanceof Conflict.ConstraintViolation v) {
                System.out.println("task " + v.task + ": required " + v.required + ", got " + v.actual);
            }
        }
        if (!result.converged) {
            System.out.println("planner hit maxIterations");
        }
    }
}
```

Dependency cycles are accepted state. Cycles in the `parent` hierarchy are different: they're invalid input and throw an [`InputException`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/input-exception.md>).

## Repair instead of reject

[`Validation.STRICT`](<https://docs.dhtmlx.com/planning-engine/java/api/project/validation.md>) suits data your own code produces. For data from a UI or an older store, [`Validation.REPAIR`](<https://docs.dhtmlx.com/planning-engine/java/api/project/validation.md>) coerces it instead of throwing:

- duplicate ids: the last record wins, at the first record's position
- missing dates or durations are derived
- in an inconsistent start/end/duration triple the dates win
- dated constraints without a date are cleared
- invalid `pauses` lists and assignment slice values are cleared

Each fix shows up as a [`Note.Repaired`](<https://docs.dhtmlx.com/planning-engine/java/api/results/note.md#repaired>) with a `path` and a `message`, so you can show the user what changed. Repair mode still rejects an entirely undated project, and other problems still throw an [`InputException`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/input-exception.md>).

```java
import org.dhtmlx.plen.*;

import java.util.List;

public class RepairMode {
    public static void main(String[] args) throws Exception {
        TaskData first = new TaskData(Id.num(1))
            .withStartDate(Moment.fromYmdHm(2026, 1, 5, 8, 0))
            .withDuration(1.0);
        TaskData duplicate = first.deepClone().withDuration(2.0);

        var project = new Project(new ProjectOptions().withValidation(Validation.REPAIR));
        ScheduleResult result = project.solve(
            new ProjectData().withTasks(List.of(first, duplicate)), new SolveOptions());
        for (Note note : result.notes) {
            if (note instanceof Note.Repaired repaired) {
                // Prints "repaired tasks[1].id: ..."
                System.out.println("repaired " + repaired.path + ": " + repaired.message);
            }
        }
    }
}
```

The [messy input how-to](<https://docs.dhtmlx.com/planning-engine/java/how-to/accept-messy-input.md>) shows the repair flow end to end.

## Errors from SkillLayer.slots

[`SkillLayer.slots`](<https://docs.dhtmlx.com/planning-engine/java/api/skill-layer-slots.md>) and [`SkillLayer.validatePool`](<https://docs.dhtmlx.com/planning-engine/java/api/skill-layer-slots.md#skilllayervalidatepool>) throw [`MatchException`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/match-exception.md>), an abstract `RuntimeException` with two nested subclasses. It isn't a `PlenException`, so `catch (PlenException e)` doesn't catch it.

| Subclass | Fields | When |
|---|---|---|
| [`MatchException.Invalid`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/match-exception.md#invalid>) | `path`, `detail` | A bad pool or requirement. `path` uses the same format as `InputException`: `pool.workers[0].resource`, `requirements[2].count`, `requirements[1].assignments[0]`. `getMessage()` is `"<path>: <detail>"`. |
| [`MatchException.Project`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/match-exception.md#project>) | `error` (the `ProjectException`), `code()` | The project has no clean solved baseline (`NO_BASELINE`). Call [`solve`](<https://docs.dhtmlx.com/planning-engine/java/api/project-solve.md>) before `slots`, including after [`apply`](<https://docs.dhtmlx.com/planning-engine/java/api/project-apply.md>). |

Java 21 also has `java.lang.MatchException`. With only `import org.dhtmlx.plen.*;`, the simple name `MatchException` is ambiguous and the code doesn't compile. Import the Planning Engine class by name:

```java
import org.dhtmlx.plen.*;
import org.dhtmlx.plen.MatchException;

import java.util.List;

public class SlotErrors {
    public static void main(String[] args) throws Exception {
        var data = new ProjectData()
            .withTasks(List.of(new TaskData(Id.num(1))
                .withStartDate(Moment.fromYmdHm(2026, 1, 5, 8, 0))
                .withDuration(2.0)))
            .withResources(List.of(new ResourceData().withId(Id.str("dev1"))));
        var project = new Project();
        project.solve(data, new SolveOptions());

        var pool = new Pool().withWorkers(List.of(new Worker().withResource(Id.str("dev1"))));
        var requirement = new Requirement(Id.str("backend"), Id.num(1), List.of()).withCount(0);

        try {
            SkillLayer.slots(project, pool, List.of(requirement));
        } catch (MatchException.Invalid e) {
            // Prints "requirements[0].count: must be at least 1"
            System.err.println(e.path + ": " + e.detail);
        } catch (MatchException.Project e) {
            System.err.println("project state: " + e.code());
        }
    }
}
```

### Handling slots and suggestions through one type

[`toPlenException()`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/match-exception.md#toplenexception>) converts either subclass into the matching engine exception. `Invalid` becomes an `InputException` with kind `VALIDATION` and the same path, and `Project` gives back the wrapped `ProjectException`. If you rethrow the converted exception, a method that calls both `slots` and [`suggestAssignments`](<https://docs.dhtmlx.com/planning-engine/java/api/project-suggest-assignments.md>) needs only one set of `catch` blocks:

```java
import org.dhtmlx.plen.*;
import org.dhtmlx.plen.MatchException;

import java.util.List;

public class Staffing {
    static SuggestResult staff(Project project, Pool pool, List<Requirement> requirements) {
        try {
            Vec<Slot> slots = SkillLayer.slots(project, pool, requirements);
            return project.suggestAssignments(slots, new SuggestOptions());
        } catch (MatchException e) {
            throw e.toPlenException();
        }
    }

    public static void main(String[] args) throws Exception {
        var pool = new Pool().withWorkers(List.of(
            new Worker()
                .withResource(Id.str("dev1"))
                .withSkills(List.of(new Skill().withId(Id.str("java")).withValue(1.0)))));
        var requirements = List.of(new Requirement(
            Id.str("backend"), Id.num(1), List.of(SkillMatch.atLeast(Id.str("java"), 1.0))));

        try {
            SuggestResult result = staff(new Project(), pool, requirements);
            System.out.println(result.steps.size() + " steps");
        } catch (InputException e) {
            System.err.println(e.path + ": " + e.getMessage());
        } catch (ProjectException e) {
            // An unsolved project: prints "project state: no-baseline".
            System.err.println("project state: " + e.getMessage());
        }
    }
}
```

One `slots` failure isn't a `MatchException`. When the summed `count` of all requirements reaches `Integer.MAX_VALUE`, `slots` throws an `InputException` (kind `VALIDATION`, `path` `null`) before it allocates anything.

## Errors in the JSON API

[`JsonProject`](<https://docs.dhtmlx.com/planning-engine/java/api/json/json-project.md>) and the [`Wire`](<https://docs.dhtmlx.com/planning-engine/java/api/json/wire.md>) calls don't throw engine errors. They catch every `PlenException` and return an error envelope in place of the result:

```json
{"error":{"code":null,"kind":"validation","message":"invalid duration","path":"tasks[0].duration"},"ok":false}
```

The `error` object has four fields:

| Field | Value |
|---|---|
| `kind` | `parse` (malformed JSON, a missing required field, a type mismatch), `validation` or `project` |
| `message` | The exception's `getMessage()` |
| `path` | The `InputException` path, or `null` |
| `code` | `no-baseline`, `stale-baseline` or `needs-resync` for `kind: "project"`, otherwise `null` |

[`Wire.slots`](<https://docs.dhtmlx.com/planning-engine/java/api/json/wire.md#slots>) and [`JsonProject#slots`](<https://docs.dhtmlx.com/planning-engine/java/api/json/json-project.md#slots>) convert a `MatchException` the same way, so the skill layer's errors arrive as `validation` or `project` envelopes too.

The one JSON call that throws is the [`JsonProject(String optionsJson)`](<https://docs.dhtmlx.com/planning-engine/java/api/json/json-project.md#jsonprojectstring>) constructor. Invalid options throw a [`JsonProjectException`](<https://docs.dhtmlx.com/planning-engine/java/api/json/json-project-exception.md>), since there's no envelope to return:

```java
import org.dhtmlx.plen.*;

public class JsonErrors {
    public static void main(String[] args) throws Exception {
        var project = new JsonProject();
        String reply = project.solve(
            "{\"tasks\":[{\"id\":1,\"start_date\":\"2026-01-05 08:00\",\"duration\":-1}]}", "{}");
        System.out.println(reply); // {"error":{...,"kind":"validation",...},"ok":false}

        try {
            new JsonProject("{\"validation\":\"lenient\"}");
        } catch (JsonProjectException e) {
            System.err.println(e.getErrorJson());                         // the error envelope
            System.err.println(e.getError().getClass().getSimpleName()); // InputException
        }
    }
}
```

`getErrorJson()` returns the same envelope the other calls would return, and `getError()` returns the `PlenException` behind it.

Only `PlenException` becomes an envelope. A JDK exception from a programming error, such as `NullPointerException`, still propagates out of `Wire` and `JsonProject`. [`Decode.fromJson`](<https://docs.dhtmlx.com/planning-engine/java/api/json/decode.md#fromjson>) is a typed call, so it throws its `InputException` (kind `PARSE` or `VALIDATION`) instead of returning an envelope. The [JSON how-to](<https://docs.dhtmlx.com/planning-engine/java/how-to/exchange-json-with-a-client.md>) shows a full endpoint.

## JDK exceptions

Outside the engine exceptions, the library throws standard JDK exceptions only for misuse of its types:

| Exception | When |
|---|---|
| `NullPointerException` | A public method gets `null` for an argument that isn't documented as nullable. [`Id.str(null)`](<https://docs.dhtmlx.com/planning-engine/java/api/data/id.md#str>) throws it too, and so does a [`Patch.of(null)`](<https://docs.dhtmlx.com/planning-engine/java/api/changes/patch.md#of>) on a field that can't be cleared that way, when the patch is applied. |
| `IllegalArgumentException` | [`Execution.parallel(threads)`](<https://docs.dhtmlx.com/planning-engine/java/api/evaluate/execution.md#parallel>) with `threads <= 0`, [`Moment.parse`](<https://docs.dhtmlx.com/planning-engine/java/api/data/moment.md#parse>) of an invalid date, [`Json.parse`](<https://docs.dhtmlx.com/planning-engine/java/api/json/json.md#parse>) of malformed text, and [`Json.stringify`](<https://docs.dhtmlx.com/planning-engine/java/api/json/json.md#stringify>) of a non-finite number |
| `IllegalStateException` | [`Patch#value()`](<https://docs.dhtmlx.com/planning-engine/java/api/changes/patch.md#value>) on a patch without a value, [`Id#key()`](<https://docs.dhtmlx.com/planning-engine/java/api/data/id.md#key>) (and `equals` / `hashCode`) of an infinite number, [`ScheduleResult#acceptedId()`](<https://docs.dhtmlx.com/planning-engine/java/api/results/schedule-result.md#acceptedid>) without a baseline, and [`Encode.text`](<https://docs.dhtmlx.com/planning-engine/java/api/json/encode.md#text>) of a non-finite number |
| `UnsupportedOperationException` | [`Encode.text`](<https://docs.dhtmlx.com/planning-engine/java/api/json/encode.md#text>) of an object that has no wire encoder |
| `OutOfMemoryError` | [`SkillLayer.slots`](<https://docs.dhtmlx.com/planning-engine/java/api/skill-layer-slots.md>) with requirement counts that are huge but still below the slot limit |

These point at a bug in the calling code, so you rarely catch them. Avoid them up front instead:

```java
import org.dhtmlx.plen.*;

public class PatchSafe {
    public static void main(String[] args) throws Exception {
        TaskUpdate update = new TaskUpdate(Id.num(1)).withDuration(Patch.of(3.0));

        // tryGetValue never throws; update.startDate.value() would, because the field is missing.
        var start = new Ref<Moment>();
        if (update.startDate.tryGetValue(start)) {
            System.out.println("new start " + start.value);
        }
        var duration = new Ref<Double>();
        if (update.duration.tryGetValue(duration)) {
            System.out.println("new duration " + duration.value);
        }

        // Moment.fromYmdHm returns null for an impossible date instead of throwing.
        Moment bad = Moment.fromYmdHm(2026, 2, 30, 8, 0);
        System.out.println(bad == null ? "no such date" : bad.toString());
    }
}
```

Valid input doesn't throw anything else. One known bug is a hang, not an exception: [`level`](<https://docs.dhtmlx.com/planning-engine/java/api/project-level.md>) doesn't return when an assignment asks for more units than its resource can ever supply (for example `value` 1.25 on capacity 1). The same applies to [`suggestAssignments`](<https://docs.dhtmlx.com/planning-engine/java/api/project-suggest-assignments.md>) with `level` set.
