For console.error(context, error), where context is a plain object and error is a native TypeError, stored Workers Logs events contained the structured context but omitted the exception's name, message and stack. Repeating the failing request with wrangler tail --format json attached captured the native error in both message and errorInfo.
This prevents diagnosis of a handled failure from retained logs unless a live tail was attached. The application did not stringify, wrap or nest the error before passing it as the second console argument.
Observed behavior
On 2026-09-26, a deployed Worker caught TypeError: Invalid header value. while constructing a request and logged it using this shape:
console.error({ message: "Request construction failed", ...context }, error);
The message above is generalized; application-specific context and identifiers are omitted.
- Two stored console events retrieved through the Workers Observability telemetry query API contained only
level, the contextual message, and the context objects. Neither exposed the exception name, message, stack or errorInfo. $metadata.error repeated the contextual summary; $workers.truncated was false.
- A subsequent attempt through the same catch site, captured with live tail, included
"TypeError: Invalid header value." as message[1]. Its errorInfo[0] was null; errorInfo[1] contained name: "TypeError", message: "Invalid header value.", and the stack frames identifying the failing constructor call.
- These were repeated requests, not a stored/live comparison of the exact same invocation. No claim is made here about how other argument orders behave in stored logs.
Expected behavior
The retained log should preserve the structured context and the native exception's name, message and stack. Cloudflare's exception logging announcement says Workers Observability enriches console logs with structured error information. The live tail demonstrates that the runtime captured that information in this case.
Minimal reproduction to isolate argument order
The following standalone comparison has not been deployed to reproduce the stored-log discrepancy. It uses only synthetic values, makes no outbound requests and needs no secrets or bindings. It is intended to compare live and stored records from the same invocation, independently of the application where the discrepancy was observed.
Save this as workers-logs-structured-error.mjs:
export default {
fetch() {
const probeId = crypto.randomUUID();
const error = new TypeError(`LOG_ERROR_SENTINEL:${probeId}`);
const context = {
message: "Structured error logging probe",
probeId,
operation: "synthetic",
};
console.error({ ...context, variant: "context-only" });
console.error({ ...context, variant: "object-first" }, error);
console.error(error, { ...context, variant: "error-first" });
console.error(`string-first ${probeId}`, error);
console.error(error);
return Response.json({ probeId });
},
};
Use this wrangler.jsonc in a disposable directory. The compatibility settings match the affected Worker:
{
"name": "workers-logs-structured-error-probe",
"main": "workers-logs-structured-error.mjs",
"compatibility_date": "2026-07-27",
"compatibility_flags": [
"nodejs_compat",
"nodejs_compat_do_not_populate_process_env",
"enable_request_signal"
],
"workers_dev": true,
"observability": {
"enabled": true,
"logs": {
"enabled": true,
"head_sampling_rate": 1,
"invocation_logs": true
}
}
}
- Deploy the disposable Worker with Wrangler and enable a JSON live tail before sending a request.
- Send one request to its deployed URL and record the returned
probeId.
- In the live tail, compare the five console calls and their positional
errorInfo entries.
- In Workers Observability, open that invocation and inspect all five stored console events, including their raw fields. Use the invocation association rather than filtering exclusively on
probeId, since some cases carry it only in a string.
- Check whether each native-error case retains
LOG_ERROR_SENTINEL, TypeError and the stack, and whether the object cases retain their named context fields. Repeat with a current compatibility date if the baseline reproduces.
The context-only call is a control and is not expected to have exception metadata. A local smoke test with Miniflare 4.20260730.0 executed all five calls and returned HTTP 200 with a probe ID; the local console displayed the synthetic exceptions. This verifies the example executes, not the hosted Workers Logs ingestion result.
Environment
- Observed service: deployed Cloudflare Workers, not local Miniflare.
- Compatibility date and flags: as shown above.
- Workers Logs enabled, head sampling rate
1; source-map upload enabled in the deployment configuration.
- Diagnostic CLI: Wrangler
4.138.0, Node.js 26.10.0, macOS 27.0, ARM64.
- The hosted runtime build is unknown. The source analysis below uses a public workerd release, not a claim about the deployed build.
Related reports and scope
workerd#6926 reports an error-first call whose stored log retains stack frames but loses the exception name and message. Here, the object-first call retains the context and exposes no exception details in the retrieved stored event. These may share an ingestion defect; I have not established whether they are the same bug.
workerd#5968 concerns an error nested inside a structured object. This report passes the native error as a separate top-level argument instead.
In the inspected workerd source, Worker::handleLog() serializes context and scans every top-level argument for native errors before calling tracer.addLog(). The runtime tests cover positional native errors mixed with strings and objects. This agrees with the live tail evidence, but does not establish where the hosted persistence path loses the information. Please route this report to Workers Observability if the relevant ingestion code is outside workerd.
References
For
console.error(context, error), wherecontextis a plain object anderroris a nativeTypeError, stored Workers Logs events contained the structured context but omitted the exception's name, message and stack. Repeating the failing request withwrangler tail --format jsonattached captured the native error in bothmessageanderrorInfo.This prevents diagnosis of a handled failure from retained logs unless a live tail was attached. The application did not stringify, wrap or nest the error before passing it as the second console argument.
Observed behavior
On 2026-09-26, a deployed Worker caught
TypeError: Invalid header value.while constructing a request and logged it using this shape:The message above is generalized; application-specific context and identifiers are omitted.
level, the contextualmessage, and the context objects. Neither exposed the exception name, message, stack orerrorInfo.$metadata.errorrepeated the contextual summary;$workers.truncatedwasfalse."TypeError: Invalid header value."asmessage[1]. ItserrorInfo[0]wasnull;errorInfo[1]containedname: "TypeError",message: "Invalid header value.", and the stack frames identifying the failing constructor call.Expected behavior
The retained log should preserve the structured context and the native exception's name, message and stack. Cloudflare's exception logging announcement says Workers Observability enriches console logs with structured error information. The live tail demonstrates that the runtime captured that information in this case.
Minimal reproduction to isolate argument order
The following standalone comparison has not been deployed to reproduce the stored-log discrepancy. It uses only synthetic values, makes no outbound requests and needs no secrets or bindings. It is intended to compare live and stored records from the same invocation, independently of the application where the discrepancy was observed.
Save this as
workers-logs-structured-error.mjs:Use this
wrangler.jsoncin a disposable directory. The compatibility settings match the affected Worker:{ "name": "workers-logs-structured-error-probe", "main": "workers-logs-structured-error.mjs", "compatibility_date": "2026-07-27", "compatibility_flags": [ "nodejs_compat", "nodejs_compat_do_not_populate_process_env", "enable_request_signal" ], "workers_dev": true, "observability": { "enabled": true, "logs": { "enabled": true, "head_sampling_rate": 1, "invocation_logs": true } } }probeId.errorInfoentries.probeId, since some cases carry it only in a string.LOG_ERROR_SENTINEL,TypeErrorand the stack, and whether the object cases retain their named context fields. Repeat with a current compatibility date if the baseline reproduces.The context-only call is a control and is not expected to have exception metadata. A local smoke test with Miniflare
4.20260730.0executed all five calls and returned HTTP200with a probe ID; the local console displayed the synthetic exceptions. This verifies the example executes, not the hosted Workers Logs ingestion result.Environment
1; source-map upload enabled in the deployment configuration.4.138.0, Node.js26.10.0, macOS27.0, ARM64.Related reports and scope
workerd#6926 reports an error-first call whose stored log retains stack frames but loses the exception name and message. Here, the object-first call retains the context and exposes no exception details in the retrieved stored event. These may share an ingestion defect; I have not established whether they are the same bug.
workerd#5968 concerns an error nested inside a structured object. This report passes the native error as a separate top-level argument instead.
In the inspected workerd source,
Worker::handleLog()serializes context and scans every top-level argument for native errors before callingtracer.addLog(). The runtime tests cover positional native errors mixed with strings and objects. This agrees with the live tail evidence, but does not establish where the hosted persistence path loses the information. Please route this report to Workers Observability if the relevant ingestion code is outside workerd.References
errorInfoarray.Worker::handleLog()at v1.20260926.1 β captures structured context and top-level native errors separately.