# How to Write Custom Operations (Ops) in Deno: A Complete Guide

> Learn to write custom Rust operations ops in Deno easily. This guide shows how to define, register, and call Deno ops from JavaScript using #[op2] and JsRuntime for seamless integration.

- Repository: [Deno/deno](https://github.com/denoland/deno)
- Tags: how-to-guide
- Published: 2026-02-26

---

**You write custom operations in Deno by defining a Rust function annotated with `#[op2]`, registering it in an `Extension`, and injecting that extension into a `JsRuntime`, making the function callable from JavaScript via `Deno.core.ops`.**

Deno's JavaScript runtime is built on the **deno_core** crate, which bridges Rust and V8 through a mechanism called *operations* (ops). If you need to expose native functionality to JavaScript in the denoland/deno repository or in a custom embedding, you must write custom operations that follow the specific patterns established in the core architecture.

## Understanding Deno Operations Architecture

Deno exposes native functionality to JavaScript through ops—Rust functions that receive an `&mut OpState` and return a `Result<T, E>`. The `#[op2]` macro generates the boilerplate that marshals V8 arguments, performs type conversion, and registers the function with the extension system.

| Component | Role | Source |
|---|---|---|
| **`OpState`** | Holds per‑runtime state (resource table, permissions, etc.). Ops can read/write data via this structure. | [[`runtime/worker.rs`](https://github.com/denoland/deno/blob/main/runtime/worker.rs)](https://github.com/denoland/deno/blob/main/runtime/worker.rs) |
| **`#[op2]` macro** | Generates boilerplate that marshals V8 arguments, performs type conversion (`FromV8`/`ToV8`), and registers the function name with the extension. | defined in the **deno_core** crate |
| **`Extension`** | A collection of ops and optional JavaScript/ESM sources. Extensions are passed to `JsRuntime` (or a worker) during creation. | [[`runtime/shared.rs`](https://github.com/denoland/deno/blob/main/runtime/shared.rs)](https://github.com/denoland/deno/blob/main/runtime/shared.rs) |
| **`JsRuntime` / Workers** | The actual V8 isolate. When constructed it receives a list of `Extension`s, merges the op tables, and makes the ops callable from JavaScript. | [[`runtime/worker.rs`](https://github.com/denoland/deno/blob/main/runtime/worker.rs)](https://github.com/denoland/deno/blob/main/runtime/worker.rs) |
| **Existing Ops** | Real‑world examples (file‑system, networking, timers, …) that you can copy‑paste and adapt. | e.g. [[`ext/fs/ops.rs`](https://github.com/denoland/deno/blob/main/ext/fs/ops.rs)](https://github.com/denoland/deno/blob/main/ext/fs/ops.rs) |

## Step-by-Step Guide to Writing Custom Ops in Deno

### Define the Rust Function with #[op2]

Create a Rust function and annotate it with `#[op2]`. Choose the appropriate variant for your use case:

- **`#[op2]`** for synchronous operations
- **`#[op2(async)]`** for asynchronous operations returning a `Future`
- **`#[op2(fast)]`** for optimized synchronous calls

Use argument attribute shortcuts to map V8 types: `#[string]` for JavaScript strings, `#[smi]` for small integers, `#[buffer]` for ArrayBuffers, and `#[number]` for floating-point values. Always return `Result<T, AnyError>` where `T` implements `ToV8`.

### Build an Extension to Register the Op

Wrap your op in an `Extension` using the builder pattern. In [`runtime/shared.rs`](https://github.com/denoland/deno/blob/main/runtime/shared.rs), you can see how Deno constructs its built-in extensions. For custom ops, create a function that returns `Extension`:

```rust
use deno_core::{Extension, OpState, op2, AnyError};

#[op2]
#[string]
pub fn op_echo(state: &mut OpState, msg: &str) -> Result<String, AnyError> {
    Ok(msg.to_owned())
}

pub fn echo_extension() -> Extension {
    Extension::builder()
        .ops(vec![op_echo::decl()])
        .build()
}

```

### Inject the Extension into JsRuntime

In [`runtime/worker.rs`](https://github.com/denoland/deno/blob/main/runtime/worker.rs), the `JsRuntime` is instantiated with a vector of extensions. To make your op available, push your extension into this vector before creating the runtime:

```rust
use deno_core::JsRuntime;

fn main() {
    let mut runtime = JsRuntime::new(Default::default());
    runtime.extensions.push(echo_extension());
    
    runtime
        .execute_script("<anon>", r#"Deno.core.ops.op_echo("Hello from JS!")"#)
        .unwrap();
}

```

### Call the Op from JavaScript

All registered ops are automatically exposed under `Deno.core.ops`. The JavaScript name is derived from the Rust function name (snake_case is preserved as-is in the ops object). For the `op_echo` example above:

```javascript
const result = Deno.core.ops.op_echo("🔧 custom op");
console.log(result); // → "🔧 custom op"

```

### Handle Async Operations and Resources

For asynchronous operations, use `#[op2(async)]` and return a `Future`. The macro automatically converts the result to a JavaScript Promise:

```rust
use deno_core::{op2, AnyError};
use std::time::Duration;
use tokio::time::sleep;

#[op2(async)]
#[number]
pub async fn op_sleep(ms: u64) -> Result<(), AnyError> {
    sleep(Duration::from_millis(ms)).await;
    Ok(())
}

```

To manage persistent state like file handles or network sockets, implement the `Resource` trait and store instances in `OpState`'s resource table. As shown in [`ext/fs/ops.rs`](https://github.com/denoland/deno/blob/main/ext/fs/ops.rs), you can add resources with `state.resource_table.add()` and retrieve them with `state.resource_table.get::<T>(rid)`.

## Complete Code Examples for Custom Deno Ops

### Synchronous String Reversal Op

This example demonstrates a minimal synchronous op that receives a string and returns a transformed value:

```rust
use deno_core::{Extension, OpState, op2, AnyError};

#[op2]
#[string]
pub fn op_reverse(state: &mut OpState, input: &str) -> Result<String, AnyError> {
    Ok(input.chars().rev().collect())
}

pub fn reverse_extension() -> Extension {
    Extension::builder()
        .ops(vec![op_reverse::decl()])
        .build()
}

```

### Asynchronous Sleep Op

This pattern shows how to handle async I/O or timers without blocking the V8 thread:

```rust
use deno_core::{Extension, op2, AnyError};
use std::time::Duration;
use tokio::time::sleep;

#[op2(async)]
#[number]
pub async fn op_sleep(ms: u64) -> Result<(), AnyError> {
    sleep(Duration::from_millis(ms)).await;
    Ok(())
}

pub fn sleep_extension() -> Extension {
    Extension::builder()
        .ops(vec![op_sleep::decl()])
        .build()
}

```

### Custom Resource Counter Op

This advanced example implements a resource that persists across multiple op calls, similar to how file descriptors work in [`ext/fs/ops.rs`](https://github.com/denoland/deno/blob/main/ext/fs/ops.rs):

```rust
use deno_core::{Extension, OpState, op2, Resource, ResourceId, AnyError};
use std::cell::RefCell;
use std::rc::Rc;

struct Counter(i64);

impl Resource for Counter {}

#[op2]
pub fn op_counter_new(state: &mut OpState) -> Result<ResourceId, AnyError> {
    let rid = state.resource_table.add(Counter(0));
    Ok(rid)
}

#[op2]
pub fn op_counter_inc(state: &mut OpState, #[smi] rid: ResourceId) -> Result<i64, AnyError> {
    let mut counter = state.resource_table.get_mut::<Counter>(rid)?;
    counter.0 += 1;
    Ok(counter.0)
}

pub fn counter_extension() -> Extension {
    Extension::builder()
        .ops(vec![
            op_counter_new::decl(),
            op_counter_inc::decl(),
        ])
        .build()
}

```

**JavaScript usage:**

```javascript
const { op_counter_new, op_counter_inc } = Deno.core.ops;
const rid = op_counter_new();
console.log(op_counter_inc(rid)); // 1
console.log(op_counter_inc(rid)); // 2

```

## Common Pitfalls When Writing Deno Ops

| Issue | Why it happens | Fix |
|---|---|---|
| **Op not found at runtime** | The extension wasn’t added to the runtime’s `extensions` vector. | Ensure `runtime.extensions.push(your_extension())` is called before executing scripts. |
| **Type mismatch errors** | Arguments aren’t annotated correctly (`#[string]`, `#[smi]`, etc.) or the return type can’t be converted. | Use the macros documented in `deno_core` (see the source of existing ops for patterns). |
| **Permission errors** | Ops that touch the filesystem or network must call the permissions container (as built‑ins do). | Use `state.borrow_mut::<deno_permissions::PermissionsContainer>()` to check permissions before performing privileged actions. |
| **Resource leaks** | Forgetting to close resources added to the resource table. | Implement `Resource` for your custom type and let the runtime drop it, or manually call `resource_table.close(rid)`. |

## Key Source Files for Deno Op Development

These files in the denoland/deno repository illustrate the full lifecycle of custom operations:

| File | Why it matters |
|---|---|
| [[`runtime/worker.rs`](https://github.com/denoland/deno/blob/main/runtime/worker.rs)](https://github.com/denoland/deno/blob/main/runtime/worker.rs) | Defines `JsRuntime`, the `extensions` field, and the plumbing that merges op tables from all extensions. |
| [[`runtime/shared.rs`](https://github.com/denoland/deno/blob/main/runtime/shared.rs)](https://github.com/denoland/deno/blob/main/runtime/shared.rs) | Shows how the core extensions (flags, runtime_main, etc.) are built with `Extension::builder()`. |
| [[`runtime/snapshot.rs`](https://github.com/denoland/deno/blob/main/runtime/snapshot.rs)](https://github.com/denoland/deno/blob/main/runtime/snapshot.rs) | Demonstrates creating a snapshot that includes custom extensions – useful for embedding Deno. |
| [[`ext/fs/ops.rs`](https://github.com/denoland/deno/blob/main/ext/fs/ops.rs)](https://github.com/denoland/deno/blob/main/ext/fs/ops.rs) | Real‑world op implementations (both sync & async) with permission checks, resource handling, and the `#[op2]` macro usage. |

## Summary

- **Custom operations** bridge Rust and JavaScript in Deno through the `deno_core` crate.
- Use the **`#[op2]` macro** to define ops with automatic V8 type marshaling for sync, async, or fast calls.
- Register ops in an **`Extension`** using `Extension::builder()` and inject it into **`JsRuntime`** before script execution.
- Access ops from JavaScript via **`Deno.core.ops`** using the snake_case function name.
- Manage persistent state with **`OpState`** and the **resource table** for handles that survive multiple calls.

## Frequently Asked Questions

### What is the difference between #[op2] and the older op macro?

The `#[op2]` macro is the modern replacement for the original `#[op]` macro in `deno_core`. It provides improved type safety, better performance optimizations through the `#[op2(fast)]` variant, and clearer attribute syntax for argument conversion such as `#[string]` and `#[smi]`. All new custom operations in the denoland/deno repository use `#[op2]`.

### How do I pass complex objects between JavaScript and Rust in Deno ops?

For complex objects, use the `serde` serialization traits. Define a Rust struct deriving `Deserialize` for incoming data and `Serialize` for outgoing data, then accept or return the struct in your op function. The `#[op2]` macro automatically handles the conversion between V8 objects and Rust structs when the type implements the appropriate serde traits.

### Can I write custom ops without modifying the Deno source code?

Yes, you can write custom ops for custom embeddings of `deno_core` without modifying the main denoland/deno repository. Create a separate Rust project that depends on `deno_core`, define your ops and extensions, and construct a `JsRuntime` with your extensions. This approach is common for building specialized JavaScript runtimes that extend Deno's capabilities.

### How do I handle permissions in custom Deno operations?

Custom ops that perform privileged actions should check permissions using the `PermissionsContainer` stored in `OpState`. Access the permissions via `state.borrow_mut::<deno_permissions::PermissionsContainer>()` and call methods like `check_read()` or `check_write()` before performing filesystem or network operations. This mirrors the security model used in built-in ops found in [`ext/fs/ops.rs`](https://github.com/denoland/deno/blob/main/ext/fs/ops.rs).