---
title: "Node.js 循环 import 为什么会在启动时抛错"
date: 2026-11-13
slug: node-esm-cycle-initialization
canonical: https://kernelranch.com/blog/2026/11/node-esm-cycle-initialization
markdown_url: https://kernelranch.com/blog/2026/11/node-esm-cycle-initialization.md
tags: ["nodejs", "esm", "architecture"]
---两个模块相互 import，启动时出现 Cannot access before initialization。先找顶层求值时谁读了尚未初始化的绑定。循环依赖可以加载，某条提前读取的执行路径会触发异常。

## 1. 生成一对失败模块和一组修复模块

保存为 esm_cycle_lab.mjs，运行 node esm_cycle_lab.mjs。脚本使用临时目录，每组在独立 Node 进程运行。

```javascript
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

const dir = mkdtempSync(join(tmpdir(), 'esm-cycle-'));
function write(name, content) {
  writeFileSync(join(dir, name), content);
}
function run(name) {
  return execFileSync(process.execPath, [join(dir, name)], {
    encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
  }).trim();
}

try {
  write('entry.mjs', "import { label } from './view.mjs'; export const env = 'prod'; console.log(label);");
  write('view.mjs', "import { env } from './entry.mjs'; export const label = 'env=' + env;");
  assert.throws(() => run('entry.mjs'), error =>
    error.status !== 0 && /before initialization/.test(error.stderr));
  console.log('cyclic startup: ReferenceError before initialization');

  write('settings.mjs', "export const env = 'prod';");
  write('view.mjs', "import { env } from './settings.mjs'; export const label = 'env=' + env;");
  write('entry.mjs', "import { label } from './view.mjs'; console.log(label);");
  assert.equal(run('entry.mjs'), 'env=prod');
  console.log('acyclic startup: env=prod');
} finally {
  rmSync(dir, { recursive: true, force: true });
}
```

本机实测输出如下。

```text
cyclic startup: ReferenceError before initialization
acyclic startup: env=prod
```

## 2. 顺着入口查顶层求值

实验以 entry 为入口。view 在顶层拼接字符串时需要读取 env，此时 entry 的 const 还未初始化。重新组织后，settings 不依赖业务模块，求值顺序为 settings、view、entry。

常见触发点包括模块顶层的实例构造、注册表初始化和静态字段读取。把共享常量移至叶子模块，或由入口构造依赖并通过参数传给业务函数。单纯把读取移进函数只能改变读取时间，仍需要确认调用时相关模块已完成初始化。

| 验收点 | 做法 |
|---|---|
| 启动路径 | 用部署命令直接启动实际入口 |
| 多入口项目 | 分别从服务、CLI 和 worker 入口测试 |
| 构建产物 | 对实际部署文件复测模块求值 |

## 小结

异常指向的是访问时机。先找顶层读取位置，再沿依赖关系将共享配置与应用组装拆开。

## 数据来源与检索时间

检索时间 2026-11-13。

| 来源 | URL | 对应内容 |
|---|---|---|
| ECMAScript Modules | https://tc39.es/ecma262/multipage/ecmascript-language-scripts-and-modules.html#sec-cyclic-module-records | 循环模块记录与求值 |
| Node.js v24.19.0 ESM | https://nodejs.org/download/release/v24.19.0/docs/api/esm.html | 使用 .mjs 启动 ESM |

## 核验说明

- Node.js v24.19.0 分别验证循环依赖报错和移出配置后的启动。未纳入打包器或 CommonJS。
