How to Fix: Can’t compile “using” keyword in Turbopack Dev
Turbopack Dev fails on the using keyword because its parser/transformation pipeline does not fully support ECMAScript Explicit Resource Management yet, even when your code is otherwise valid for newer JavaScript runtimes.
If you hit a compile error in Next.js development mode with Turbopack after adding using, the issue is usually not your business logic. It is a tooling compatibility gap: the dev bundler cannot parse or transform that syntax path correctly, while other compilers or runtimes may.
Understanding the Root Cause
The using keyword comes from the Explicit Resource Management proposal in JavaScript. It introduces structured cleanup semantics, typically for objects implementing disposal hooks such as Symbol.dispose or Symbol.asyncDispose.
The problem in this GitHub issue is that Turbopack Dev is responsible for parsing and transforming source files during development. If its parser, syntax flags, or internal transform stages lag behind the proposal support implemented elsewhere, the build fails before runtime. In practice, that means:
- Your code may be syntactically valid according to newer JavaScript proposal support.
- Next.js dev mode with Turbopack may still reject it.
- The error appears during compilation, not because disposal logic is wrong, but because the bundler cannot handle the syntax.
This is especially common with proposal-stage features that require coordinated support across multiple layers:
- The JavaScript parser
- The transformer/compiler
- The dev bundler
- The runtime target
- Any Babel or SWC compatibility path
So the root cause is simple: Turbopack Dev currently does not fully support the using syntax path used in the reproduction.
Step-by-Step Solution
Until Turbopack adds support, the safest fix is to avoid the using keyword in development builds that go through Turbopack and replace it with explicit cleanup patterns.
1. Identify the unsupported syntax
You will typically have code like this:
class Resource implements Disposable {
[Symbol.dispose]() {
console.log('disposed');
}
}
export function runTask() {
using resource = new Resource();
console.log('working');
}
This is the syntax that triggers the compile failure in Turbopack Dev.
2. Replace using with try/finally
Use a manual disposal pattern instead. This preserves the intent and works with current tooling.
class Resource {
[Symbol.dispose]() {
console.log('disposed');
}
}
export function runTask() {
const resource = new Resource();
try {
console.log('working');
} finally {
resource[Symbol.dispose]?.();
}
}
This is the most direct workaround because try/finally is universally supported by the toolchain.
3. Handle async cleanup explicitly when needed
If your resource cleanup is asynchronous, mirror await using behavior with try/finally and await.
class AsyncResource {
async [Symbol.asyncDispose]() {
console.log('async disposed');
}
}
export async function runTask() {
const resource = new AsyncResource();
try {
console.log('working');
} finally {
await resource[Symbol.asyncDispose]?.();
}
}
4. If possible, disable Turbopack for local development
If your project depends on proposal syntax and you need a short-term workflow fix, switch to the stable webpack-based dev flow if your setup allows it. For example, use the standard Next.js development command rather than a Turbopack-enabled variant configured in your project.
npm run dev
Then check your project scripts in package.json. If you see a Turbopack flag, remove it temporarily.
{
"scripts": {
"dev": "next dev"
}
}
If your current script explicitly enables Turbopack, it may look like this:
{
"scripts": {
"dev": "next dev --turbo"
}
}
Change it to the non-Turbopack version until upstream support lands.
5. Keep the feature isolated behind an abstraction
If you want future support with minimal refactoring later, wrap disposal logic in a helper today.
export function disposeSafely(resource) {
resource?.[Symbol.dispose]?.();
}
export async function disposeSafelyAsync(resource) {
await resource?.[Symbol.asyncDispose]?.();
}
Then use it consistently:
import { disposeSafely } from './dispose';
class Resource {
[Symbol.dispose]() {
console.log('disposed');
}
}
export function runTask() {
const resource = new Resource();
try {
console.log('working');
} finally {
disposeSafely(resource);
}
}
This keeps your codebase readable and makes it easier to migrate back to using later.
6. Track upstream support
Because this is a tooling bug, the long-term fix is an update in Turbopack, Next.js, or the underlying parser/transform stack. Watch the related GitHub issue and release notes rather than overengineering a custom compiler workaround. If you need the original reproduction, use the provided CodeSandbox example.
Common Edge Cases
- Async disposal mismatch: If the original code conceptually used
await using, replacing it with synchronous cleanup can cause leaked resources or race conditions. Useawaitin thefinallyblock when needed. - Missing disposal symbols: Calling
resource[Symbol.dispose]()without optional chaining may throw if the object does not implement the hook. Prefer?.()unless the interface is guaranteed. - Server/client boundary issues: In a Next.js app, resource management code may belong only on the server. If it accidentally lands in a client component, you can see unrelated bundling or runtime errors that obscure the real syntax problem.
- Mixed compiler expectations: Babel plugins, TypeScript settings, SWC behavior, and runtime support may all differ. Even if one tool accepts the syntax, Turbopack Dev may still fail first.
- Polyfill confusion: Adding a
Symbol.disposepolyfill does not solve a parser-level syntax failure. The issue happens before execution. - Production vs development differences: Some teams observe different behavior between dev and build pipelines. Even if another path succeeds, treat Turbopack compile support as its own compatibility target.
FAQ
Does this mean the using keyword is invalid JavaScript?
No. It means your current dev toolchain does not support that syntax path yet. The language feature and the bundler support timeline are separate concerns.
Can I fix this with a TypeScript or Babel setting?
Usually not in this specific case. If Turbopack fails during parsing or its own transform stage, configuration changes in adjacent tools often do not help. The practical workaround is rewriting the code without using or disabling Turbopack for development.
What is the best production-safe replacement for using right now?
Use try/finally with explicit calls to Symbol.dispose or Symbol.asyncDispose. It is readable, standards-aligned in intent, and compatible with today’s stable JavaScript tooling.
In short, this issue is not caused by incorrect cleanup code. It is a Turbopack syntax support gap. The immediate solution is to replace using with explicit disposal logic or temporarily avoid Turbopack Dev until upstream support is released.