[Canonical page](<https://docs.dhtmlx.com/planning-engine/js/how-to/recover-from-rejected-changes/>)
[Full documentation index](<https://docs.dhtmlx.com/planning-engine/llms.txt>)


# Recover from rejected changes

Get an editor working again after [`apply`](<https://docs.dhtmlx.com/planning-engine/js/api/project-apply.md>) rejects part of a change set, without restarting the app or losing the user's edits.

When [`apply`](<https://docs.dhtmlx.com/planning-engine/js/api/project-apply.md>) rejects an operation, the report has `needsResync: true`. From then on, `solve(null)`, `apply` and `level` throw [`ProjectError`](<https://docs.dhtmlx.com/planning-engine/js/api/types/errors.md#projecterror>) with code `"needs-resync"`. Calls that need a baseline (`preview`, `loadProfile`, `evaluate`, `suggestAssignments`) throw `ProjectError` with code `"no-baseline"`. To clear the flag, send the full data again with [`solve(data)`](<https://docs.dhtmlx.com/planning-engine/js/api/project-solve.md>). [Editing a project](<https://docs.dhtmlx.com/planning-engine/js/live-projects/editing-a-project.md#rejections-and-resync>) explains why.

## Prerequisites

- A long-lived [`Project`](<https://docs.dhtmlx.com/planning-engine/js/api/project.md>) that you solved once with [`solve(data)`](<https://docs.dhtmlx.com/planning-engine/js/api/project-solve.md>).
- Your own model that can produce the full [`ProjectData`](<https://docs.dhtmlx.com/planning-engine/js/api/types/project-data.md>) the user expects to see. You resync from this data.

## Steps

1. Apply the change set and keep the report:

   ```js
   const report = project.apply(changes);
   ```

   If `apply` throws, nothing was applied. [`ProjectError`](<https://docs.dhtmlx.com/planning-engine/js/api/types/errors.md#projecterror>) means `"needs-resync"` from an earlier rejection or `"stale-baseline"`. [`InputError`](<https://docs.dhtmlx.com/planning-engine/js/api/types/errors.md#inputerror>) means a malformed change set. A rejection inside the batch doesn't throw.

2. Check `report.needsResync`. If it's `false`, continue as usual with [`solve(null)`](<https://docs.dhtmlx.com/planning-engine/js/api/project-solve.md>):

   ```js
   if (!report.needsResync) {
     project.solve(null);
   }
   ```

3. Show the user what was refused. Each entry of `report.rejected` has the operation's `path` (such as `tasks.remove[0]`) and a `message`:

   ```js
   for (const op of report.rejected) {
     console.error(`rejected ${op.path}: ${op.message}`);
   }
   ```

   The other operations in the batch were applied. A rejected operation leaves no partial edit behind.

4. Make your model hold the state you want. Undo the rejected edits in it, or keep it as it is if it's already correct. Don't try to repair the project with more [`apply`](<https://docs.dhtmlx.com/planning-engine/js/api/project-apply.md>) calls, because they throw `"needs-resync"`.

5. Resync with the full data. Set [`includeUnchanged`](<https://docs.dhtmlx.com/planning-engine/js/api/types/solve-options.md>) so the result has the dates of every task, not only the ones that moved:

   ```js
   const result = project.solve(model, { includeUnchanged: true });
   ```

6. Write every entry of `result.updates` back into your UI. The engine's state can differ from what the UI shows, so refresh all task dates, not only the edited ones.

7. Use the new `result.baselineId` for the next [`preview`](<https://docs.dhtmlx.com/planning-engine/js/api/project-preview.md>) or change set. Tokens from before the resync are stale.

The full example below stretches one task. In the same batch, it removes a task the engine doesn't have:

```js
import { InputError, Project } from "plen-js";

// The editor's own model: the data you resync from.
const model = {
  tasks: [{ id: "design", start_date: "2026-01-05", duration: 2 }],
  // 24/7 time keeps the example dates easy to check.
  config: { use_calendars: false },
};

const project = new Project();
project.solve(model);

// The user stretches "design" to 4 days. The UI also sends a removal
// of "review", a task the engine never had.
model.tasks[0].duration = 4;
const report = project.apply({
  tasks: {
    update: [{ id: "design", duration: 4 }],
    remove: ["review"],
  },
});

if (!report.needsResync) {
  project.solve(null);
} else {
  for (const op of report.rejected) {
    console.error(`rejected ${op.path}: ${op.message}`);
  }

  try {
    const result = project.solve(model, { includeUnchanged: true });
    for (const update of result.updates) {
      console.log(update.id, `${update.start_date}..${update.end_date}`);
    }
  } catch (error) {
    if (!(error instanceof InputError)) throw error;
    // The project keeps its state and still needs a resync.
    console.error(`resync data invalid at ${error.path}: ${error.message}`);
  }
}
```

## When the resync data is invalid

The resync goes through the same validation as the first load. If your model produces invalid data, such as a duplicate task id, [`solve(data)`](<https://docs.dhtmlx.com/planning-engine/js/api/project-solve.md>) throws [`InputError`](<https://docs.dhtmlx.com/planning-engine/js/api/types/errors.md#inputerror>) with the field path, for example `tasks[1].id`. The project keeps its previous state, and `solve(null)`, `apply` and `level` still throw `"needs-resync"`. Fix the data and call `solve(data)` again. See [Error handling](<https://docs.dhtmlx.com/planning-engine/js/error-handling.md>) for the full list of validation errors.

## Drop the edit instead of resyncing

To avoid a full reload, apply the change set to a [`clone()`](<https://docs.dhtmlx.com/planning-engine/js/api/project.md#clone>) first. A rejection on the clone doesn't touch the original:

```js
const draft = project.clone();
const report = draft.apply(changes);
if (report.needsResync) {
  // Drop `draft`. `project` still has its baseline; roll the edit back in your model too.
} else {
  draft.solve(null);
  project = draft;
}
```

Use this when the user should get either the whole change set or none of it.

> **Note:** `clone()` copies the whole project at once, so its cost grows with the project's size.

## Result

The example prints the rejected removal to stderr and the refreshed dates of every task to stdout:

```text
rejected tasks.remove[0]: unknown task id
design 2026-01-05 00:00..2026-01-09 00:00
```

After the resync, [`apply`](<https://docs.dhtmlx.com/planning-engine/js/api/project-apply.md>), [`solve(null)`](<https://docs.dhtmlx.com/planning-engine/js/api/project-solve.md>), `level` and [`preview`](<https://docs.dhtmlx.com/planning-engine/js/api/project-preview.md>) work again, and `report.needsResync` is `false` for the next valid change set.
