Skip to content

Lazy evaluation

LogTape provides several mechanisms for lazy evaluation—deferring the evaluation of values until they're actually needed for logging. This feature serves two critical purposes: performance optimization and dynamic value tracking.

Why lazy evaluation?

Lazy evaluation is useful in two key scenarios:

Performance optimization
Avoid expensive computations when logs are disabled. For example, if you're logging detailed debug information that requires serializing large objects or performing complex calculations, you don't want to pay that cost when debug logging is turned off.
Dynamic values
Capture values at logging time rather than logger creation time. This is essential when you need to log values that change over time, such as user session data that gets loaded after logger initialization.

Dynamic context with lazy()

The lazy() function is available since LogTape 2.0.0.

The lazy() function allows you to defer the evaluation of context values until logging time. This is particularly useful for dynamic or mutable context that changes over the lifetime of your application.

The problem: static context

Consider this common pattern in single-page applications:

import {  } from "@logtape/logtape";

let : User | null = null;

// Logger is created early, before user data is available
const  = ("app").({
  :   // This captures null immediately
});

const  = .("feature");

// Later, user data is loaded
 = await ();

// But logs still show user: null because context was captured earlier
.("User action");  // user: null (not what we want!)

The problem is that with() captures the current value of currentUser at the time it's called. Child loggers inherit that captured value, so they never see the updated user data.

The solution: lazy context

Use lazy() to defer value evaluation until logging time:

import { ,  } from "@logtape/logtape";

let : User | null = null;

// lazy() wraps a function that will be called at logging time
const  = ("app").({
  : (() => 
    ? { : ., : . }
    : null
  )
});

const  = .("feature");

// No user yet
.("Initialization");  // user: null

// User data loads
 = await ();

// Now logs reflect the current user
.("User action");  // user: { id: 1, isAdmin: true }

// User data changes
. = false;

// Logs always show the latest value
.("Another action");  // user: { id: 1, isAdmin: false }

The lazy() wrapper defers evaluation—the callback is invoked at logging time, not at with() time. Since child loggers inherit the lazy() wrapper itself (not its resolved value), they always get the latest value.

Real-world patterns

Request correlation IDs

import { ,  } from "@logtape/logtape";

let : string | null = null;

const  = ("api").({
  : (() => )
});

function (: Request) {
   = .();
  // All logs in this request will include the same requestId
  .("Processing request");
  // ...
   = null;
}

Environment-dependent values

import { ,  } from "@logtape/logtape";

const  = ("app").({
  // Always log the current environment, even if it changes at runtime
  : (() => ..),
  // Log the current memory usage
  : (() => .().)
});

Performance optimization with lazy()

Beyond dynamic values, lazy() is also useful for avoiding expensive computations when logs are disabled:

.("Query result", {
  // This serialization only happens if debug logging is enabled
  : (() => .())
});

Without lazy(), JSON.stringify() would run even when debug logging is disabled, wasting CPU cycles. With lazy(), the serialization only happens if the log message is actually going to be recorded.

When to use lazy() for performance

Consider using lazy() when:

  • Serializing large objects or arrays
  • Performing expensive string formatting
  • Computing derived values that require significant processing
  • Accessing properties that might throw exceptions
.("Processing batch", {
  : .,
  // Only format items if debug logging is enabled
  : (() => .( => formatItem())),
  // Only call expensive method if needed
  : (() => .())
});

Async lazy evaluation

Async lazy evaluation is available since LogTape 2.0.0.

For asynchronous operations like database queries or API calls, you can pass async functions directly as property values in structured logging:

await .("User activity", {
  // This async function only executes if debug logging is enabled
  : async () => await ()
});

Note that async lazy evaluation requires using await with the log method, and it only works with property values in structured logging—you cannot use async functions in template literals.

For more details on async lazy evaluation, see Structured logging docs.

Conditional logging with isEnabledFor()

This API is available since LogTape 2.0.0.

When you need to conditionally execute multiple log statements or perform setup work only when logging is enabled, use the isEnabledFor() method:

import {  } from "@logtape/logtape";
const  = (["my-app"]);

if (.("debug")) {
  // All of this only runs if debug logging is enabled
  const  = ();
  const  = ();
  const  = ();

  .("State analysis: {report}", {  });
  .("Raw snapshot: {snapshot}", {  });
}

function () { return {}; }
function (: unknown) { return {}; }
function (: unknown) { return ""; }

The isEnabledFor() method is more efficient than lazy() for multiple log statements because it avoids the overhead of function calls. However, for single property values, lazy() provides a cleaner syntax.

For more details, see Quick start.

Choosing the right approach

Use this guide to choose the appropriate lazy evaluation mechanism:

Decision tree

  1. Is the value asynchronous (requires await)?

    • Yes: Use async lazy evaluation (async function as property value)
    • No: Continue to question 2
  2. Do you need to conditionally execute multiple log statements?

  3. Does the value change over time or require expensive computation?

    • Yes: Use lazy()
    • No: Use the value directly

Comparison table

ApproachUse whenExample
Direct valueValue is cheap to compute and static{ count: items.length }
lazy()Synchronous expensive computation or dynamic value{ data: lazy(() => JSON.stringify(obj)) }
Async functionAsynchronous operation needed{ user: async () => fetchUser(id) }
isEnabledFor()Multiple log statements or setup workif (logger.isEnabledFor("debug")) { ... }

Performance considerations

Overhead of lazy evaluation

Lazy evaluation introduces minimal overhead:

  • lazy() wraps a function call, adding negligible overhead (~nanoseconds)
  • The main benefit comes from avoiding expensive operations, not from the wrapper itself
  • Async lazy evaluation has slightly higher overhead due to promise handling

Common anti-patterns

Avoid these patterns that defeat the purpose of lazy evaluation:

// ❌ BAD: Computing the value before passing it to lazy()
const  = .();  
.("Data", { : (() => ) });
// ✅ GOOD: Let lazy() defer the computation
.("Data", { : (() => .()) });
// ❌ BAD: Using lazy() for cheap operations
.("Count", { : (() => ) });  
// ✅ GOOD: Use direct values for cheap operations
.("Count", { :  });

See also

  • Contexts: Detailed information about logger contexts and the with() method
  • Structured logging: More about structured logging and async lazy evaluation
  • Quick start: Introduction to isEnabledFor() method

Released under the MIT License.