> ## Documentation Index
> Fetch the complete documentation index at: https://docs.marioded.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Threading and failure handling

> Handle asynchronous completion, missing data, and runtime lifecycle without blocking Bukkit.

# Threading and failure handling

Many Reverse operations use `CompletionStage` or `CompletableFuture`. Let's separate two questions: **where do I call the operation?** and **where does its completion callback run?**

## Start on the right thread

Call recording start/stop and live playback/UI controls on the Bukkit main thread. Let the service move storage work off that thread.

```java theme={null}
CompletionStage<StopResult> saving = recorder.getRecorderService().stopSession(sessionId);
```

This stops capture now and returns a stage for persistence. It does not require moving `stopSession` itself into an async task.

| Operation | Rule |
| - | - |
| Recorder start/stop and `saveRecentClip`/`saveClip` | Invoke on Bukkit's main thread; persistence completion has no thread guarantee. |
| Loaded playback start, seek, pause, speed, leave/stop | Main thread. |
| `play(UUID, ...)` | Loads asynchronously and starts on the main thread; completion callbacks have no thread guarantee. |
| Catalog/storage stages | No main-thread completion guarantee. |
| Layout service and inventory action callbacks | Main thread; do not block. |
| Save-completion events | Can run on the save-completion thread. |
| Catalogue/dispatch events | Run directly on the operation's completion thread, or inline for immediate outcomes; no automatic main-thread dispatch. |
| Explicit recorder observation APIs | Designed for capture/network threads; obey their contract and do not query Bukkit positions there. |
| Chunk payload transfer | Provider is thread-safe and asynchronous; world-source snapshot setup has its own main-thread contract. |

## Handle both completion and failure

Inside a JavaPlugin callback, we can handle a stop result without touching player/world state:

```java theme={null}
saving.whenComplete((result, failure) -> {
    if (failure != null) {
        getLogger().log(Level.SEVERE, "Save failed for " + sessionId, failure);
        return;
    }
    if (result.replayId() == null) {
        getLogger().warning("The session was no longer recording");
        return;
    }
    if (result.degraded()) {
        getLogger().log(Level.WARNING, "Replay saved with terrain failure", result.terrainFailure());
        return;
    }
    getLogger().info("Replay ready: " + result.replayId());
});
```

An exceptional completion is not the only non-success outcome. Conversely, a missing/forbidden catalog entry is a normal result, not necessarily an exception.

Notification coverage differs by operation: a metadata backend failure completes the stage exceptionally without `ReplayMetadataUpdatedEvent`; a deletion stage failure also emits `ReplayDeletedEvent` with its cause. A dispatch exception emits `TRANSFER_FAILED` and may leave the stage exceptional, while some failures are converted to a normal result. Synchronous validation/setup failures can occur before any notification hook is attached. See the [event contracts](https://docs.marioded.com/api/events.md), and retain stage handling wherever your integration must observe every outcome.

## Schedule Bukkit work back to the main thread

Suppose `viewerId` identifies the player to notify after saving. Capture the ID before asynchronous work, then look up the current player on the main thread:

```java theme={null}
saving.whenComplete((result, failure) -> {
    if (!isEnabled()) return;
    getServer().getScheduler().runTask(this, () -> {
        Player current = getServer().getPlayer(viewerId);
        if (current == null || !current.isOnline()) return;
        if (failure != null || result.replayId() == null || result.degraded()) {
            current.sendMessage("This recording is not ready for normal playback.");
            return;
        }
        current.sendMessage("Replay ready: " + result.replayId());
    });
});
```

This snippet belongs inside your JavaPlugin class. It avoids Bukkit player lookup in the completion thread. A real request flow should also track whether this is still the viewer's latest request and access policy.

If disabling can race task submission in your integration, handle scheduler rejection during teardown too. There is no guarantee that a future finishes before your plugin begins disabling.

## Do not wait for I/O in a normal server tick

Avoid this pattern in a command, match callback, or inventory action:

```java theme={null}
// Do not use in a normal main-thread callback: it waits for backend I/O.
saving.toCompletableFuture().join();
```

Attach a completion handler instead. Moving the backend wait to the main thread defeats the asynchronous API and can stall the server.

## Catch synchronous validation failures where they happen

Not every error arrives through a future. For example, user-supplied metadata is validated synchronously:

```java theme={null}
try {
    ReplayCatalog.validateMetadata(name, description);
} catch (IllegalArgumentException invalid) {
    viewer.sendMessage("Choose a shorter plain-text name and description.");
    return;
}
```

Do that in your main-thread command/menu layer before calling start/update. Keep backend exceptions in the async failure path. Do not report every exception as “not found.”

## Respect runtime ownership

Services belong to the Reverse runtime. Your plugin owns the match-to-session IDs, listeners, request guards, and custom registrations it creates.

* Stop the recordings your matches own on end/cancellation/disable paths.
* Do not call `stopAll` just to clean up your own match when other integrations may be recording.
* Remove your menu actions/items and retire hotbar callbacks as described in [UI extensions](https://docs.marioded.com/api/ui-extensions.md).
* Do not close borrowed storage/availability or reinitialize its packet adapter.

For optional Reverse support, isolate role-specific classes and listeners until that role is available. Packaging another API copy does not make an absent runtime integration work.

## Complete examples

The [first recording tutorial](https://docs.marioded.com/api/first-recording.md) handles all save outcomes. The [playback example](https://docs.marioded.com/examples/playback-integration.md) demonstrates main-thread completion handling, and the [event example](https://docs.marioded.com/examples/replay-notifications.md) shows save-notification scheduling.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.