---
title: "Call C# from JavaScript"
url: https://recursiveai.org/oneecma/cookbook/host-functions/
description: "Give scripts C# functions with OneEcma’s host API: plain functions, .NET exceptions that scripts can catch, tasks that become promises, and script values…"
version: "1.0.0"
---

# Call C# from JavaScript

Give scripts C# functions with OneEcma’s host API: plain functions, .NET exceptions that scripts can catch, tasks that become promises, and script values read back in C#. No generator needed.

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

var stock = new Dictionary<string, int> { ["pen"] = 12, ["ink"] = 0 };

var engine = new ScriptEngine(new ScriptEngineOptions
{
    // Host setup goes in Configure: a failed run is replayed to explain it, and the replay rebuilds this world.
    Configure = e =>
    {
        JsRealm realm = e.MainRealm;

        // A plain function: (this, args) -> value.
        realm.DefineData(realm.Global, "stockOf", JsValue.FromObject(realm.NewFunction("stockOf", 1, (_, args) =>
        {
            if (args.Length == 0 || !args[0].IsString)
                throw new JsException(realm.NewError("TypeError", "stockOf: expected a SKU string, got " + realm.Describe(args.Length > 0 ? args[0] : JsValue.Undefined)));
            return stock.TryGetValue(args[0].AsString, out int n) ? JsValue.FromNumber(n) : JsValue.Null;
        })), JsAttributes.Method);

        // A .NET exception thrown inside a host function reaches script as a catchable Error.
        realm.DefineData(realm.Global, "reserve", JsValue.FromObject(realm.NewFunction("reserve", 2, (_, args) =>
        {
            string sku = args[0].AsString;
            int qty = (int)args[1].AsNumber;
            if (stock[sku] < qty) throw new InvalidOperationException($"only {stock[sku]} {sku} left");
            stock[sku] -= qty;
            return JsValue.FromNumber(stock[sku]);
        })), JsAttributes.Method);

        // A Task becomes a Promise; script awaits it.
        realm.DefineData(realm.Global, "priceOf", JsValue.FromObject(realm.NewFunction("priceOf", 1, (_, args) =>
        {
            string sku = args[0].AsString;
            return JsMarshal.FromTask(realm, "priceOf", async () =>
            {
                await Task.Delay(10);                                    // a database or HTTP call
                return sku == "pen" ? 1.5 : 4.25;
            }, (_, price) => JsValue.FromNumber(price));
        })), JsAttributes.Method);
    },
});

Console.WriteLine(engine.ToDisplayString(engine.Evaluate("stockOf('pen') + ' pens, ' + stockOf('nib') + ' nibs'", "stock.js")));

Console.WriteLine(engine.ToDisplayString(engine.Evaluate("""
    try { reserve('ink', 2); } catch (e) { `${e.name}: ${e.message} (${e.dotnetType})` }
    """, "reserve.js")));

JsValue promise = engine.Evaluate("""
    (async () => {
      const price = await priceOf('pen');
      reserve('pen', 4);
      return `4 pens: $${price * 4}, ${stockOf('pen')} left`;
    })()
    """, "order.js");
Console.WriteLine(engine.ToDisplayString(await engine.AwaitAsync(promise)));

// Read script values back in C#.
JsValue cart = engine.Evaluate("({ sku: 'pen', qty: 3, tags: ['office'] })", "cart.js");
JsObject obj = cart.AsObject!;
Console.WriteLine($"{engine.MainRealm.Get(obj, "sku").AsString} x {engine.MainRealm.Get(obj, "qty").AsNumber}, {engine.MainRealm.Describe(cart)}");

// A .NET exception nobody catches arrives in C# as a JsException with the original inside.
try
{
    engine.Evaluate("reserve('ink', 1);", "uncaught.js");
}
catch (JsException e)
{
    Console.WriteLine($"InnerException: {e.InnerException?.GetType().Name}: {e.InnerException?.Message}");
    Console.WriteLine(DiagnosticRenderer.Render(e.Report!));
}
```

```text
12 pens, null nibs
Error: InvalidOperationException: only 0 ink left (System.InvalidOperationException)
4 pens: $6, 8 left
pen x 3, an object with keys sku, qty, tags
InnerException: InvalidOperationException: only 0 ink left
error[Error]: InvalidOperationException: only 0 ink left
 --> uncaught.js:1:1
  |
1 | reserve('ink', 1);
  | ^^^^^^^
  |
  = note: thrown by .NET code: System.InvalidOperationException in Program.<lambda>
  = stack: Program.<lambda> (Program.cs:26) ← <top-level> (uncaught.js:1)
```

## How it works

- **Functions.** `realm.NewFunction(name, length, fn)` makes a function; `DefineData` puts it on the global object. `JsAttributes.Method` makes it writable and configurable but not enumerable, like a built-in. `engine.DefineGlobalFunction(name, length, fn)` is a shortcut that the record log also remembers.
- **Throwing to script.** `throw new JsException(realm.NewError("TypeError", "…"))` throws a real script error. Any other .NET exception becomes an `Error` whose message is `<Type>: <message>`, with the full type name in a non-enumerable `dotnetType` property.
- **Exceptions reaching C#.** A script error is a `JsException` with the thrown `Value` and the `Report`. When it started as a .NET exception, `InnerException` is the original, and the report’s stack interleaves .NET frames (`Program.<lambda> (Program.cs:26)`) with script frames. Under NativeAOT, .NET frames show method names only.
- **Tasks.** `JsMarshal.FromTask` turns a `Task<T>` into a promise. While your async method runs, its continuations are routed back to the engine’s thread, so it may call into script safely. Wait with `AwaitAsync`, or pump the engine with `RunUntilIdle`.
- **Reading values.** `JsValue` is a 16-byte struct: test it with `IsString`, `IsNumber`, `IsObject` and read it with `AsString`, `AsNumber`, `AsObject`. `realm.Get`, `Set`, `OwnKeys` and `Call` run the language’s own operations, so getters and proxy traps run.

## Good to know

- An engine isn’t thread-safe. Host callbacks run on the engine’s thread.
- A script that calls a host function spends fuel for the call, so a loop over a host call is still bounded by the step limit. The time spent inside your .NET code is yours to bound.
- To expose whole classes, records and enums without writing each function, use the generator: [Expose .NET objects with [JsExport]](https://recursiveai.org/oneecma/cookbook/jsexport/).
- The full reference is [the host API](https://recursiveai.org/oneecma/docs/host-api/).
