Chessalyzer

Handling errors

Stop on the first broken game, or skip it and keep going.

Real-world PGN files are messy. Somewhere in those two million games there's usually one with a move that doesn't make sense. You choose what happens when Chessalyzer meets it: stop immediately, or skip the game and keep going.

To try both, imagine our example file with one broken game appended — 2. Nf9 is not a square a knight can reach:

[Event "Broken game"]
[White "blunderbuss"]
[Black "cleopatra"]
[Result "*"]

1. e4 e5 2. Nf9 0-1

Stop on the first broken game (default)

By default, analyzePGN throws as soon as a game can't be replayed. That's what you want while developing — you notice bad data immediately:

import { analyzePGN, isReplayError } from 'chessalyzer';

try {
    await analyzePGN('games.pgn', { trackers: [tiles] });
} catch (err) {
    if (isReplayError(err)) {
        console.error(`Game ${err.gameIndex}, move ${err.moveIndex}: ${err.san}`);
        // Game 3, move 2: Nf9
    }
    throw err;
}

isReplayError narrows the thrown value so you can read gameIndex (which game, zero-based), moveIndex (which half-move), san, and a machine-readable reason.

Skip bad games and keep going

For batch runs over mostly-good data (think Lichess database dumps), aborting on one bad apple wastes the whole run. onError: 'skip-game' collects the failures and finishes the rest:

const result = await analyzePGN('games.pgn', {
    trackers: [tiles],
    onError: 'skip-game',
});

The three good games are fully processed, and the result tells you exactly what was skipped:

{
    gameCount: 3,
    moveCount: 19,
    skippedGames: 1,
    errors: [
        {
            code: 'replay',
            gameIndex: 3,
            moveIndex: 2,
            san: 'Nf9',
            reason: 'IllegalMove',
            message: 'w: No piece for move N to (7,6) found!',
        },
    ],
    runs: [{ gameCount: 3, moveCount: 19, skippedGames: 1, errors: [/* same entry */] }],
}

(perf.durationMs and perf.movesPerSecond omitted — they're there too inside result.perf. Each entry in runs mirrors its own skippedGames/errors when you use multiple runs, omitted when zero.)

Two limits to know: errors holds at most 100 entries — past that, result.errorsTruncated is true and counting continues silently — and Chessalyzer never prints anything by itself, so what happens with these errors is entirely your call.

Errors only surface when games are replayed

Replay trusts your PGN — it applies moves, but it does not check them for legality, and in a count-only run (no move trackers) games aren't replayed at all, so a broken move may pass unnoticed. That's the deliberate speed/strictness trade-off. If you're cleaning unknown data, attach any move tracker (or set replay: 'board') to make sure every move is actually decoded.

On this page