---
title: "Record a failed run and replay it"
url: https://recursiveai.org/oneecma/cookbook/record-replay/
description: "Save the record log of a JavaScript run that failed in production and replay it anywhere with OneEcma: the same clock readings, random numbers and…"
version: "1.0.0"
---

# Record a failed run and replay it

Save the record log of a JavaScript run that failed in production and replay it anywhere with OneEcma: the same clock readings, random numbers and host-call results come back from the log, so the replay fails the same way and gives the same report, with each frame’s locals added.

The script below fails only sometimes: half of the time `Math.random()` picks a discount code that doesn’t exist. The program records runs until one fails, saves that run’s log, and replays it from the file in a second process.

```csharp
using RecursiveAI.OneEcma;
using RecursiveAI.OneEcma.Diagnostics;

const string script = """
    function pickDiscount() {
      const codes = { SPRING: 0.1, VIP: 0.2 };
      const roll = Math.random();
      return roll < 0.5 ? codes.SPRING : codes.VIP_PLUS;
    }
    const rate = exchangeRate("EUR");
    const price = (100 * (1 - pickDiscount()) * rate).toFixed(2);
    if (price === "NaN") throw new RangeError(`price is NaN at ${new Date().toISOString()}`);
    price;
    """;

if (args is ["replay", string path])
{
    // Later, anywhere: no host functions, no clock, no randomness. Everything comes from the log.
    RecordLog saved = RecordLog.FromArray(File.ReadAllBytes(path));
    ReplayResult replay = ScriptEngine.Replay(saved);
    Console.WriteLine($"replayed {replay.Entries} entry call(s), divergence: {replay.Divergence ?? "none"}");
    Console.WriteLine(DiagnosticRenderer.Render(replay.Report!, new RenderOptions { ShowLocals = true }));
    return;
}

// Record runs until one fails, then keep its log.
for (int attempt = 1; ; attempt++)
{
    var engine = new ScriptEngine();
    engine.DefineGlobalFunction("exchangeRate", 1, (_, a) => JsValue.FromNumber(0.92));   // remembered by the log
    try
    {
        JsValue price = engine.Evaluate(script, "price.js");
        Console.WriteLine($"run {attempt}: {engine.ToDisplayString(price)}");
    }
    catch (JsException e)
    {
        byte[] log = engine.RecordLog.ToArray();
        File.WriteAllBytes("failure.oerl", log);
        Console.WriteLine($"run {attempt} failed; saved {log.Length} bytes, {engine.RecordLog.EventCount} events, replayable: {engine.RecordLog.IsReplayable}");
        Console.WriteLine(DiagnosticRenderer.Render(e.Report!));
        break;
    }
}
```

```shell
dotnet run
```

```text
run 1 failed; saved 462 bytes, 6 events, replayable: True
error[RangeError]: price is NaN at 2026-10-27T19:17:21.167Z
 --> price.js:8:32
  |
8 | if (price === "NaN") throw new RangeError(`price is NaN at ${new Date().toISOString()}`);
  |                                ^^^^^^^^^^
  |
  = stack: <top-level> (price.js:8)
```

How many runs pass before one fails depends on the random numbers. Then, in another process, with no `exchangeRate` function defined:

```shell
dotnet run -- replay failure.oerl
```

```text
replayed 1 entry call(s), divergence: none
error[RangeError]: price is NaN at 2026-10-27T19:17:21.167Z
 --> price.js:8:32
  |
8 | if (price === "NaN") throw new RangeError(`price is NaN at ${new Date().toISOString()}`);
  |                                ^^^^^^^^^^
  |
  = stack: <top-level> (price.js:8)
  = locals in <top-level>: rate = 0.92, price = "NaN"
```

The replay throws at the same place with the same timestamp in the message, because `new Date()` read the recorded clock. Replaying the same file twice gives byte-identical output.

## What the log holds

Recording is on by default: `engine.RecordLog` is the log of everything the run took from outside the engine, in order. That is every script and module evaluated, clock readings, `Math.random()` results, time zone and locale answers, host function results and exceptions, the settling of host tasks, job-queue drains and weak-reference collections. It costs a few appends per host interaction and nothing per instruction. A run with no host interaction logs its source plus about 20 bytes, `Math.random()` costs 9 bytes a call, and the log stops growing at 8 MiB by default (`new RecordLog(maxBytes)` through `ScriptEngineOptions.Log`).

`ScriptEngine.Replay(log)` builds a fresh engine whose host answers from the log alone. Host functions don’t run again, so their side effects don’t happen twice. Every event is checked against what the script asks for, and `ReplayResult.Divergence` says what differed if the code, the host functions or the engine changed since the recording.

## Good to know

- This is the same mechanism that explains errors: when a run fails, the engine replays it with tracing on to find where values came from. That is why host setup belongs in `ScriptEngineOptions.Configure`, which a replay calls again; pass the same `Configure` in `ReplayOptions` when you replay a log yourself.
- Functions made with `engine.DefineGlobalFunction` are rebuilt from the log, as `exchangeRate` is here.
- `RecordLog.IsReplayable` is false, with the reason in `NotReplayableReason`, after things a log can’t reproduce, such as using a second realm or a log that outgrew its limit.
- `ReplayOptions.StopAfterEntry` stops after a given entry call, and `ReplayResult.Engine` is the replayed engine, for inspecting state at that point.
- The byte format (version 1) and every event are in [the record log](https://recursiveai.org/oneecma/docs/record-log/).
