workerd Error Handling and Exception Reporting: From C++ KJ Exceptions to JavaScript Errors
workerd bridges C++ and JavaScript errors by wrapping low-level kj::Exception objects with JSG macros that prefix error types, then converting them to native V8 Error objects via the exceptionToJs function in src/workerd/jsg/util.c++.
The cloudflare/workerd runtime powers Cloudflare Workers by executing JavaScript in V8 isolates backed by high-performance C++ infrastructure. Understanding workerd error handling and exception reporting is essential for runtime developers extending native APIs or debugging boundary issues between C++ system code and Worker scripts.
The Three-Layer Error Model in workerd
workerd implements a tiered architecture that translates low-level system failures into standard JavaScript exceptions that behave identically to native ECMAScript errors.
Layer 1: KJ Exceptions for C++ Failures
At the foundation, workerd uses kj::Exception from the KJ C++ library to represent failures such as file I/O errors, assertion failures, and internal logic violations. This class is defined in the external src/kj/debug.h dependency and serves as the canonical error type throughout the C++ codebase.
Layer 2: JSG Macros as the Bridge
The JSG (JavaScript Glue) layer provides macros in src/workerd/jsg/exception.h that enrich kj::Exception objects with JavaScript-compatible metadata. These macros prepend specific prefixes like jsg.TypeError: or jsg.Error: to the exception description, allowing downstream code to instantiate the correct JavaScript error constructor.
// src/workerd/jsg/exception.h
#define JSG_FAIL_REQUIRE(jsErrorType, ...) \
KJ_FAIL_REQUIRE(kj::str(JSG_EXCEPTION(jsErrorType) ": ", ##__VA_ARGS__))
When you invoke JSG_FAIL_REQUIRE(TypeError, "message"), the macro generates a kj::Exception whose description begins with jsg.TypeError: message. Related macros like JSG_REQUIRE, JSG_ASSERT, and JSG_WARN_ONCE provide variations that either maintain process continuity or log warnings without throwing.
Layer 3: V8 Error Object Conversion
The exceptionToJs function in src/workerd/jsg/util.c++ performs the final transformation from C++ exceptions to V8 JavaScript objects. This function parses the prefix injected by JSG macros, strips it to extract the human-readable message, and invokes the appropriate V8 factory method:
// src/workerd/jsg/util.c++ (conceptual)
v8::Local<v8::Value> exceptionToJs(
v8::Isolate* isolate,
kj::Exception&& exception,
ExceptionToJsOptions options) {
const kj::StringPtr desc = exception.getDescription();
auto errType = parseJsErrorType(desc); // extracts "jsg.TypeError"
v8::Local<v8::String> msg =
v8::String::NewFromUtf8(isolate, desc.slice(errType.size() + 2).cStr())
.ToLocalChecked();
if (errType == "jsg.TypeError") {
return v8::Exception::TypeError(msg);
} else if (errType == "jsg.Error") {
return v8::Exception::Error(msg);
}
// ... additional error type branches
}
Schema Parsing Error Collection
For configuration errors encountered while loading Cap'n Proto schemas (such as malformed workerd.capnp files), workerd uses the SchemaFileImpl::ErrorReporter class defined in src/workerd/server/workerd.c++. This reporter collects validation failures in a vector for batch display to the user, distinguishing parsing-time errors from runtime exceptions.
How exceptionToJs Transforms KJ Exceptions into V8 Errors
The conversion logic in src/workerd/jsg/util.c++ handles several specialized exception categories beyond basic type mapping. The function checks for do-not-log markers via isDoNotLogException(), which prevents duplicate logging when a Worker script has already handled the error. It also detects tunneled exceptions and internal errors using isTunneledException() and isInternal() helpers declared in src/workerd/jsg/exception.h.
For internal exceptions—operational failures that should not expose implementation details to user code—the function attaches hidden metadata to the V8 error object. This tagging enables the LOG_EXCEPTION_IF_INTERNAL macro (defined near line 1230 of src/workerd/jsg/exception.h) to write syslog entries for platform monitoring while presenting a generic message to the Worker script.
The final step in the error pipeline occurs in src/workerd/io/worker.c++ around line 1701, where the runtime calls isolate->ThrowException(exceptionToJs(...)) to abort the current JavaScript execution with the converted error object.
Error Generation Points in the Codebase
workerd generates exceptions at three primary boundaries between native code and JavaScript execution.
Runtime API Validation with JSG_REQUIRE
Most public API entry points validate arguments using JSG_REQUIRE or JSG_FAIL_REQUIRE before performing operations. For example, in src/workerd/api/web-socket.c++, an invalid accept() call triggers:
JSG_FAIL_REQUIRE(TypeError,
"Can't return WebSocket in a Response after calling accept().");
This pattern ensures that type mismatches, range errors, and illegal state transitions surface as standard JavaScript exceptions that developers can catch with standard try/catch blocks.
Configuration Parsing in SchemaFileImpl
During startup, when workerd loads Worker configurations from Cap'n Proto schemas, the ErrorReporter implementation in src/workerd/server/workerd.c++ captures syntax errors and validation failures. Unlike runtime errors that throw immediately, these parsing errors accumulate in a collection for comprehensive reporting before the process exits.
Promise Rejection Handling
For asynchronous operations, the promise wrapper in src/workerd/jsg/promise.c++ intercepts any kj::Exception thrown from C++ callbacks. It converts the exception using exceptionToJs and rejects the associated JavaScript promise with the resulting Error object, ensuring that async/await code receives exceptions through standard promise rejection channels.
How workerd Reports Errors to Worker Scripts
Once converted, errors propagate to Worker scripts through three distinct mechanisms depending on the execution context.
Immediate synchronous throws abort the current V8 call stack when isolate->ThrowException() is invoked during a JavaScript-to-C++ binding call. The V8 engine unwinds the stack until caught by a JavaScript try/catch block or the global error handler.
Promise rejections occur at asynchronous boundaries. The converted error becomes the rejection reason passed to .catch() handlers or the catch clause of an async/await sequence.
Internal logging happens conditionally through LOG_EXCEPTION_IF_INTERNAL. This macro evaluates whether an exception carries the internal flag set by exceptionToJs. User-script errors remain unlogged to prevent noise, while infrastructure failures write to Cloudflare's operational monitoring systems.
Practical Code Examples for workerd Error Handling
Raising TypeErrors from C++ APIs
When extending workerd with custom native APIs, use JSG_REQUIRE to enforce preconditions that generate catchable JavaScript errors:
#include "workerd/jsg/exception.h"
void MyDatabase::query(jsg::Lock& js, kj::StringPtr sql) {
// Enforce query length limits with a descriptive TypeError
JSG_REQUIRE(sql.size() < 10000, TypeError,
"SQL query exceeds maximum length of 10000 characters, got ", sql.size());
// ... execute query ...
}
When a Worker script calls this method with an oversized string, the runtime throws a JavaScript TypeError identical to one created via new TypeError() in standard ECMAScript.
Handling Errors in JavaScript Workers
Worker scripts catch native errors using standard JavaScript exception handling:
export default {
async fetch(request, env) {
try {
env.myDatabase.query("SELECT * FROM ".repeat(1000));
} catch (e) {
// e.name === "TypeError"
// e.message === "SQL query exceeds maximum length of 10000 characters, got 12000"
return new Response(`Invalid query: ${e.message}`, { status: 400 });
}
return new Response("Success");
}
}
Distinguishing Internal and User Errors
For operations that should never be exposed to user scripts but require platform monitoring, use the standard error macros without special prefixes, allowing the internal tagging mechanism to classify the error:
void SystemCache::evictAll() {
// This operation is restricted at the infrastructure level
JSG_FAIL_REQUIRE(Error, "Cache evictAll() disabled in production isolates");
}
According to the workerd source code, this generates an exception tagged as internal. The script receives a generic Error object, while LOG_EXCEPTION_IF_INTERNAL writes the detailed failure to Cloudflare's internal telemetry systems.
Summary
- workerd error handling operates through a three-tier architecture:
kj::Exceptionfor C++, JSG macros for metadata enrichment, andexceptionToJsfor V8 conversion. - The
exceptionToJsfunction insrc/workerd/jsg/util.c++parses error type prefixes and instantiates appropriate V8 Error, TypeError, or RangeError objects. - JSG_REQUIRE and JSG_FAIL_REQUIRE in
src/workerd/jsg/exception.hprovide the standard mechanism for C++ APIs to throw catchable JavaScript exceptions. - Internal errors are distinguished from user errors via metadata flags, enabling selective logging through
LOG_EXCEPTION_IF_INTERNALwhile preventing information leakage. - Schema parsing errors use the ErrorReporter pattern in
src/workerd/server/workerd.c++for batch validation reporting during configuration loading.
Frequently Asked Questions
What is the difference between JSG_REQUIRE and JSG_FAIL_REQUIRE in workerd?
JSG_REQUIRE evaluates a condition and throws a kj::Exception only if the condition fails, similar to an assertion with custom error types. JSG_FAIL_REQUIRE immediately throws the specified error without evaluating a condition, functioning like an unconditional throw statement. Both macros prefix the error description with strings like jsg.TypeError: that exceptionToJs parses to create the correct JavaScript error constructor.
How does workerd prevent internal error details from leaking to user scripts?
The exceptionToJs function checks for internal exceptions using the isInternal() helper and attaches a hidden flag to the V8 error object. While the script receives a standard JavaScript Error, the LOG_EXCEPTION_IF_INTERNAL macro detects this flag and routes detailed diagnostic information to Cloudflare's internal monitoring systems only, filtering it from user-visible outputs.
Where does workerd convert C++ exceptions into JavaScript Promise rejections?
The conversion happens in src/workerd/jsg/promise.c++, where the promise wrapper catches kj::Exception objects from C++ callbacks and invokes js.exceptionToJs() to create the rejection reason. This ensures that asynchronous native operations reject promises with proper JavaScript Error objects rather than failing silently or crashing the isolate.
Can I customize error messages when extending workerd's C++ APIs?
Yes, when implementing new APIs in the src/workerd/api/ directory, use the JSG_REQUIRE or JSG_FAIL_REQUIRE macros with printf-style arguments to construct dynamic error messages. The macros support variable interpolation (e.g., JSG_REQUIRE(value > 0, RangeError, "Expected positive value, got ", value)), and these messages propagate intact through exceptionToJs to the JavaScript Error.prototype.message property.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →