How to Fix: “use cache” cannot call function from inside closure

5 min read

The crash happens because Next.js “use cache” does not allow cached functions to depend on values captured from an inner closure. If a cached function references another function created inside component or request scope, Next cannot safely serialize that dependency into its cache boundary, so the build/runtime throws an error instead of caching something unstable.

Understanding the Root Cause

The key rule behind “use cache” is that the cached function must be analyzable as a stable unit of work. In practice, that means Next.js expects the cached code to depend only on:

  • its explicit arguments,
  • module-level imports,
  • serializable values, and
  • other supported stable references.

The reproduction in the issue breaks that rule by calling a function from inside a closure. A closure captures variables or functions from the surrounding scope. That is normal JavaScript behavior, but it conflicts with how React Server Components and Next.js caching reason about deterministic execution.

Why this fails technically:

  • A function defined inside another function is recreated on every render/request.
  • That inner function may capture request-specific or render-specific state.
  • “use cache” cannot reliably treat that captured function as a stable cache key input.
  • Because the dependency graph is no longer safely serializable, Next rejects it.

In short, the bug is not really that calling functions is forbidden. The real problem is calling a function that comes from a non-module, closure-scoped reference from within a cached execution boundary.

A simplified version of the problematic pattern looks like this:

export default async function Page() {
  async function innerFn() {
    return 'hello'
  }

  async function cachedFn() {
    'use cache'
    return innerFn()
  }

  return <div>{await cachedFn()}</div>
}

Even though this looks harmless, innerFn lives inside the component scope, not at module scope. The cached function therefore depends on a closure value.

Step-by-Step Solution

The fix is to move any function used by a “use cache” block out of the closure and into a module-level function, or pass plain serializable data into the cached function instead of captured functions.

1. Identify the closure-scoped dependency

Look for code where a cached function references:

  • an inner helper function,
  • a locally declared callback,
  • request-scoped variables, or
  • objects created in the component body.

Problem example:

export default async function Page() {
  const prefix = 'Hello'

  function formatMessage(name) {
    return `${prefix} ${name}`
  }

  async function getGreeting(name) {
    'use cache'
    return formatMessage(name)
  }

  return <div>{await getGreeting('Romeo')}</div>
}

2. Move the helper to module scope

Refactor the helper so the cached function uses a stable reference:

function formatMessage(prefix, name) {
  return `${prefix} ${name}`
}

async function getGreeting(prefix, name) {
  'use cache'
  return formatMessage(prefix, name)
}

export default async function Page() {
  return <div>{await getGreeting('Hello', 'Romeo')}</div>
}

This works better because:

  • formatMessage is now a module-level function,
  • getGreeting depends on explicit arguments, and
  • the cache boundary is deterministic.

3. Prefer explicit arguments over captured values

If the original inner function used variables from the outer scope, pass those values directly:

async function loadUserLabel(userId, prefix) {
  'use cache'
  return `${prefix}-${userId}`
}

export default async function Page() {
  const userId = '42'
  return <div>{await loadUserLabel(userId, 'user')}</div>
}

This pattern is safer than:

export default async function Page() {
  const prefix = 'user'

  async function loadUserLabel(userId) {
    'use cache'
    return `${prefix}-${userId}`
  }

  return <div>{await loadUserLabel('42')}</div>
}

4. Keep cached logic isolated

A reliable structure for Next.js is:

import 'server-only'

async function queryData(id) {
  return { id, name: 'Example' }
}

export async function getCachedData(id) {
  'use cache'
  return queryData(id)
}

export default async function Page() {
  const data = await getCachedData('1')
  return <div>{data.name}</div>
}

That separation makes it obvious which code is:

  • cacheable,
  • server-only, and
  • free from unstable closure captures.

5. If needed, extract to a separate module

For larger codebases, the cleanest fix is often moving cached functions and helpers into a dedicated server file:

// app/lib/data.ts
import 'server-only'

function buildKey(type, id) {
  return `${type}:${id}`
}

export async function getCachedRecord(type, id) {
  'use cache'
  return buildKey(type, id)
}
// app/page.tsx
import { getCachedRecord } from './lib/data'

export default async function Page() {
  const value = await getCachedRecord('post', '123')
  return <div>{value}</div>
}

If you want to inspect the original reproduction, use the GitHub repro repository.

Common Edge Cases

1. Capturing environment-dependent values

Even if you are not calling an inner function directly, the same failure pattern can appear when a cached function captures values that are created in request scope, such as derived headers, cookies, or per-request objects.

export default async function Page() {
  const token = Math.random().toString()

  async function cachedFn() {
    'use cache'
    return token
  }
}

This is unstable for the same reason: closure capture.

2. Wrapping database or fetch helpers inside components

A common mistake is creating a local helper in a page or layout and then calling it from a cached function. Even if the helper itself is pure, its location inside the component makes it a closure-scoped dependency.

3. Passing non-serializable values

If you fix the closure issue but still pass unsupported values such as class instances, complex streams, or mutable objects, caching can still fail or behave unpredictably. Prefer primitives and plain objects.

4. Mixing client and server concerns

“use cache” is a server-side feature. If a helper is tightly coupled to client-side code or browser APIs, moving it to module scope alone will not make it valid. Keep cached code in server-compatible modules.

5. Assuming every extracted function is safe

Moving a function to module scope fixes the closure problem, but you still need deterministic behavior. If the function reads changing global state or performs side effects that should not be cached, extraction alone is not enough.

FAQ

Can I call any helper function from a “use cache” function?

Yes, but it should ideally be a module-level helper or another stable imported function. Avoid helpers declared inside a component, route handler, or other closure-producing scope.

Why does normal JavaScript closure behavior break in Next.js caching?

Because Next.js cache analysis needs deterministic, serializable dependencies. JavaScript closures are valid language features, but they hide runtime context that the caching system cannot safely convert into a stable cache boundary.

What is the safest refactor for this issue?

The safest refactor is to move the cached function and all helper functions it calls to module scope, then pass every dynamic value as an explicit argument. That removes hidden dependencies and aligns the code with Next.js caching expectations.

Leave a Reply

Your email address will not be published. Required fields are marked *