# Slow work: files and network

> Run file IO and network requests off the main thread with runAsync, show progress and report the result in a StatusLine.

URL: https://xichil.dev/docs/xis-mvui/v1/guides/async-work/ · Updated: 2026-11-06 · Applies to: Fabric, Quilt, Legacy Fabric, Forge, NeoForge, MC 1.7.10 - 26.3

Every action runs on the game's main thread, so a slow action freezes the game. Move slow work into
`host.runAsync` and report the result with a [StatusLine](https://xichil.dev/docs/xis-mvui/v1/widgets/status-line/index.md).

## The pattern

```java
StatusLine status = StatusLine.builder("status").build();

Button save = Button.builder("save")
        .label(Text.translatable("mymod.save", "Save"))
        .onPress(() -> host.runAsync(() -> {
            store.save(config);                // background thread: no game, no screen
            return null;
        }, (ignored, error) -> {                // main thread again
            if (error != null) {
                status.show(StatusLine.Kind.ERROR, Text.translatable("mymod.save.failed", "Couldn't save: %s",
                        Text.literal(String.valueOf(error.getMessage()))));
            } else {
                status.show(StatusLine.Kind.SUCCESS, Text.translatable("mymod.saved", "Saved"));
            }
        }))
        .build();

MenuScreen screen = MenuScreen.builder("main")
        .title(Text.translatable("mymod.title", "My Mod"))
        .add(status)
        .add(save)
        .build();
```

## What runAsync does

`host.runAsync(task, onDone)`:

1. Starts `task` off the main thread. Meanwhile the current screen shows **Working…**, its widgets are inactive
   and it ignores input, and `host.isBusy()` is `true`.
2. When `task` returns, runs `onDone(result, null)` on the main thread. When it throws, runs
   `onDone(null, exception)`.
3. Refreshes the screen, so the status line and every supplier show the new state.

Rules:

- `task` must not touch the game or the screen. Read what it needs into local variables before calling
  `runAsync`, and apply results in `onDone`.
- `onDone` may do anything an action may: change state, write to a `StatusLine`, open a screen.
- For no result, make the task a `Callable<Void>` and `return null;`.
- A registered menu's tasks run on a thread of their own, **one at a time, in the order they were started**. A
  task that hangs holds up only your menu, never another mod's.
- If `onDone` itself throws, the error is logged with your mod id and the menu stays open.

## A task with a result

```java
host.runAsync(() -> store.load(name), (profile, error) -> {
    if (error != null) {
        status.show(StatusLine.Kind.ERROR, Text.translatable("mymod.load.failed", "Couldn't load %s",
                Text.literal(name)));
    } else {
        config.apply(profile);
        status.show(StatusLine.Kind.SUCCESS, Text.translatable("mymod.loaded", "Loaded %s", Text.literal(name)));
    }
});
```

The result type follows from the task: here `store.load` returns the type that `profile` gets.

## Work that starts elsewhere

If your own thread or a callback from another library changes what the menu shows, get back to the main
thread with `runOnMainThread` and refresh yourself. It is the one `UiHost` method that is safe to call from any
thread:

```java
downloader.onFinished(file -> host.runOnMainThread(() -> {
    status.show(StatusLine.Kind.SUCCESS, Text.translatable("mymod.downloaded", "Downloaded"));
    host.refresh();     // runOnMainThread doesn't refresh by itself
}));
```

`StatusLine.show` and `clear` are safe from any thread too; the message appears on the next refresh.

## Status line kinds

| Kind | Use it for |
| --- | --- |
| `INFO` | neutral information |
| `SUCCESS` | the action worked |
| `WARNING` | the player should notice something, or the action partly failed |
| `ERROR` | the action failed |

Backends color the text by kind. A status line is empty until the first `show`, and `clear()` empties it.

## Test it

`FakeHost` runs async tasks synchronously, so the result is there right after the click:

```java
host.click("save");
assertTrue(host.render().contains("[status status] SUCCESS Saved"));
```

## Next steps

- [Threads](https://xichil.dev/docs/xis-mvui/v1/concepts/threads/index.md): the full threading rules.
- [Save and load settings](https://xichil.dev/docs/xis-mvui/v1/guides/saving-config/index.md): what to save and when.
