Skip to main content
workerd follows the KJ C++ style guide with some project-specific conventions. This guide covers the most important patterns and rules.

Core principles

The KJ style guide describes suggestions, not absolute rules. Consistency matters, but pragmatic exceptions are acceptable when justified.
Read the full KJ style guide and KJ tour before contributing.

Use KJ types, not STL

The project uses the KJ library instead of the C++ standard library for most types:
Instead ofUse
std::stringkj::String (owned) / kj::StringPtr (view)
std::vectorkj::Array<T> (fixed) / kj::Vector<T> (growable)
std::unique_ptrkj::Own<T>
std::shared_ptrkj::Rc<T> / kj::Arc<T> (thread-safe)
std::optionalkj::Maybe<T>
std::functionkj::Function<T>
std::variantkj::OneOf<T...>
std::spankj::ArrayPtr<T>
std::exceptionkj::Exception
std::promise/futurekj::Promise<T>
Avoid including <string>, <vector>, <memory>, <optional>, or other std headers from header files.

Memory management

Never write new or delete directly. Use KJ heap allocation:
// Allocate a single object
auto obj = kj::heap<MyType>(args...);

// Allocate an array
auto arr = kj::heapArray<int>(size);

// Build arrays incrementally
auto builder = kj::heapArrayBuilder<int>(size);
builder.add(42);
builder.add(99);
auto arr = builder.finish();

// Reference counting
auto rc = kj::rc<MyType>(args...);
auto arc = kj::arc<MyType>(args...);  // Thread-safe

Ownership model

  • Every object has exactly one owner (another object or a stack frame)
  • kj::Own<T> transfers ownership when moved
  • Raw C++ pointers and references are borrowing only, never owned
  • An object can never own itself, even transitively (no reference cycles)

Error handling

Never use throw directly. Use KJ assertion macros:
// Check invariants (bug in this code)
KJ_ASSERT(value > 0, "value must be positive", value);

// Check preconditions (bug in caller)
KJ_REQUIRE(ptr != nullptr, "pointer cannot be null");

// Unconditional failure
KJ_FAIL_ASSERT("this should never happen");

// Wrap system calls
auto fd = KJ_SYSCALL(open(path, O_RDONLY), path);

// Mark unreachable code
KJ_UNREACHABLE;
These macros automatically capture file/line, stringify operands, and generate stack traces.
Unwrapping kj::Maybe values:
// Using KJ_IF_SOME
KJ_IF_SOME(value, maybeValue) {
  // Use value here
} else {
  // Handle empty case
}

// Or with assertions
auto& value = KJ_ASSERT_NONNULL(maybeValue);

Exception philosophy

  • Exceptions are for fault tolerance, not control flow
  • They represent things that “should never happen” - bugs, network failures, resource exhaustion
  • Never declare anything noexcept - bugs can happen anywhere
  • Destructors must use noexcept(false) if they can throw

Naming conventions

KindStyle
Types (classes, structs)TitleCase
Variables, functions, methodscamelCase
Constants, enumerantsCAPITAL_WITH_UNDERSCORES
MacrosCAPITAL_WITH_UNDERSCORES with prefix
Namespacesoneword (keep short)
Filesmodule-name.c++, module-name.h

File extensions

  • C++ source files: .c++ (not .cpp)
  • Header files: .h
  • Test files: module-name-test.c++ (hyphenated suffix)

Lambda capture rules

// NEVER use [=] - makes lifetime analysis impossible
// BAD
auto bad = [=]() { return value; };

// OK for non-escaping lambdas
auto ok = [&]() { return value; };

// For escaping lambdas, be explicit
auto escaping = [value = kj::mv(value)]() { return value; };

// Capture for promises
return promise.then(kj::coCapture([](MyType& obj) {
  return obj.doSomething();
}, kj::mv(obj)));
Never use [=] (capture-all-by-value). Use [&] only when the lambda will not outlive the current stack frame.

Formatting

Run just format before committing. Key rules:
  • 2-space indents, never tabs
  • Max 100 characters per line
  • Space after keywords: if (foo), for (...), while (...)
  • No space after function names: foo(bar)
  • Always use braces for blocks (unless the entire statement fits on one line)
  • Strip trailing whitespace

Comments

workerd follows common C++ comment conventions:
// Always use // line comments, never /* */

// Put doc comments before the declaration
// This method does something important.
void doSomething();

// Never state the obvious
int count;  // BAD: stores the count

// TODO format
// TODO(now): Must be fixed before merging
// TODO(soon): Should be addressed in near future
// TODO(someday): Nice to have improvement

Text encoding

  • All text is UTF-8
  • Never assume text is valid UTF-8 - be tolerant of malformed sequences
  • Use kj::str() for string formatting
  • Never use FILE*, iostream, or C stdio

RAII and cleanup

Always prefer RAII for resource management:
// Cleanup in destructor
class FileHandle {
  ~FileHandle() {
    // One cleanup action per destructor
    KJ_SYSCALL(close(fd));
  }
  int fd;
};

// Scope-exit cleanup
void example() {
  auto file = openFile();
  KJ_DEFER(closeFile(file));
  // file automatically closed at scope exit
}

No singletons

Never use mutable globals or global registries. The main() function or high-level code should explicitly construct components and wire dependencies via constructor parameters.

Lazy validation

Validate data at time-of-use, not upfront:
// BAD: upfront validation
void processConfig(Config config) {
  validateAllFields(config);  // Wastes work
  // ...
  if (needsDatabase) {
    useDatabase(config.dbUrl);  // Only validates here
  }
}

// GOOD: validate at use
void processConfig(Config config) {
  // ...
  if (needsDatabase) {
    auto& dbUrl = KJ_REQUIRE_NONNULL(config.dbUrl, "database URL required");
    useDatabase(dbUrl);
  }
}

C++ standard

The project uses C++23 (-std=c++23). You can use modern C++ features, but prefer KJ idioms when available.

Next steps

Testing guidelines

Learn about testing requirements

JSG architecture

Understand the JavaScript binding layer

Build docs developers (and LLMs) love