Open-Source Wikis

/

Deno

/

How to contribute

/

Patterns and conventions

denoland/deno

Patterns and conventions

The conventions you'll encounter all over the Deno codebase. Following these matches existing code and shortens review cycles.

Every source file starts with:

// Copyright 2018-2026 the Deno authors. MIT license.

tools/copyright_checker.js enforces this in CI. New files without the header will fail the lint job.

Rust conventions

Module organization

  • lib.rs is the crate root; public API surface lives there.
  • Sub-modules go in sibling files (foo.rs) or sibling directories (foo/mod.rs).
  • The CLI in particular uses many sibling files at the top level of cli/ rather than a deeper module hierarchy. Don't fight that; new top-level CLI concerns can be a new sibling file.
  • Keep mod.rs files small — usually just pub mod declarations and re-exports.

Error handling

  • deno_core::anyhow::Error (re-exported as AnyError) is used for top-level error returns from CLI subcommands.
  • More structured errors use deno_error (workspace dep) with the JsErrorBox family — these are errors that get bubbled into JS land.
  • Lower-level crates (libs/*, ext/*) tend to define their own crate-specific error enums with thiserror. Match the pattern of the surrounding code.
  • Result<T, AnyError> is the workhorse return type for async CLI code paths.

Async

  • The runtime uses Tokio. Async code in the CLI side runs on current_thread runtimes (see runtime/tokio_util.rs::create_and_run_current_thread_with_maybe_metrics).
  • Long futures should be boxed_local() to avoid blowing the Windows stack in debug builds — see the comment in cli/lib.rs spawn_subcommand.
  • Cooperative cancellation goes through deno_core::CancelHandle / RcLike patterns; check existing ops for the right shape.

Ops (the JS/Rust boundary)

Every privileged or runtime-level Rust function exposed to JS is an "op", written with the #[op2] attribute macro from deno_core:

#[op2(async)]
pub async fn op_fs_open_async(
    state: Rc<RefCell<OpState>>,
    #[string] path: String,
    #[serde] options: OpenOptions,
) -> Result<ResourceId, FsError> { ... }

Conventions:

  • Op names: op_<area>_<verb> (e.g., op_fs_open_async, op_net_listen_tcp).
  • Sync ops drop the _async suffix.
  • Argument annotations (#[string], #[serde], #[buffer], #[smi], #[bigint]) tell op2 how to convert from V8 — match the convention of nearby ops.
  • Permission checks happen inside the op, not at the dispatch layer:
    state
        .borrow_mut::<PermissionsContainer>()
        .check_read(&path, "Deno.openSync()")?;

Lints and clippy

  • tools/lint.js runs cargo clippy --all-targets.
  • Many crates include a clippy.toml that adds extra restrictions (e.g., cli/clippy.toml is 2,625 bytes of project-specific rules). Honor those — they're there for a reason.
  • The custom JS-side lint plugins live in tools/lint_plugins/.

JavaScript / TypeScript conventions

Numbered files

In ext/web/, ext/fetch/, runtime/js/, etc., files are prefixed with two-digit numbers (00_*, 01_*, ...). The numbers control snapshot load order; lower numbers load first. New files should slot into the existing numbering — pick a number after all dependencies and before all dependents.

primordials / safe globals

Runtime JS code does not use the standard prototype-bound globals (Array.prototype.map, JSON.parse, etc.) because user code can monkey-patch them. Instead, code uses primordials — captured references to the original built-ins, available via deno_core's primordials support.

const { ArrayPrototypeMap, JSONParse } = primordials;
const result = ArrayPrototypeMap(arr, fn);

This pattern is a hard rule for code that runs before user code (most extension JS). When in doubt, look at the file you're editing — if it imports from primordials, follow suit.

TypeScript types for ops

Op signatures are declared in tools/ops.d.ts and per-extension internal.d.ts files. When you add a new op, also add its TypeScript declaration so the calling JS code is type-checked.

Public-facing JS APIs

User-facing JS APIs (Deno.*, the web platform globals, node:* modules) are documented inline with TSDoc comments. The doc generator pulls from these to produce https://docs.deno.com. Keep them up to date when changing behavior.

Testing conventions

  • New user-visible behavior → spec test under tests/specs/.
  • New Deno.* JS API → unit test under tests/unit/.
  • New node:* polyfill → unit test under tests/unit_node/ and possibly enable a Node compat test.
  • New op or lower-level Rust function → inline #[cfg(test)] test in the same file.
  • New LSP capability → add to tests/integration/lsp_tests.rs (yes, the 20K-line one).

Cross-cutting patterns

CliFactory

Anything in cli/ that needs configured services (module loader, npm resolver, type checker, …) goes through cli/factory.rs::CliFactory. The factory lazily constructs services from the parsed Flags and handles config-file discovery. Don't construct these services directly in subcommand code — ask the factory.

Permission checks

Privileged ops always go through runtime/permissions/lib.rs::PermissionsContainer. The pattern:

let permissions = state.borrow_mut::<PermissionsContainer>();
permissions.check_read(&path, "api_name")?;

The second argument is the API name shown in the prompt and in error messages. Make it match the user-facing function.

Flags are the source of truth

CLI flags defined in cli/args/flags.rs produce a typed Flags struct that is threaded into the rest of the CLI. New behavior that needs configuration should add a flag (and a matching deno.json field if it should be persistable) rather than reading env vars directly. Env-var-only escape hatches are reserved for things like DENO_LOG and DENO_DIR.

Sys traits

cli/lib.rs defines pub type CliSys = sys_traits::impls::RealSys; — the abstraction over filesystem/process/env that lets CLI code be tested without touching the real OS. Lower-level crates (libs/resolver, libs/cache_dir, etc.) take a generic Sys: ... parameter so tests can plug in fakes.

Style nits

  • Keep functions small and well-named; the codebase favors many short functions over long ones.
  • Comments explain why, not what. The code itself shows what.
  • cargo fmt and dprint are not negotiable — ./x verify checks them.
  • Conventional commit prefixes in PR titles: feat(scope):, fix(scope):, perf(scope):, etc.
  • No unnecessary unsafe. When unsafe is needed, comment why and what invariants the caller must uphold.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Patterns and conventions – Deno wiki | Factory