Node.js 的 require 报 ERR_REQUIRE_ASYNC_MODULE,检查依赖中的顶层 await
升级 Node 后插件启动报 ERR_REQUIRE_ASYNC_MODULE,核对顶层 await、旧加载器捕获码和异步调用链。
升级 Node.js 后,原本加载插件的代码在启动阶段报 ERR_REQUIRE_ASYNC_MODULE,还没进入插件的业务逻辑。错误通常出在同步 require() 遇到了带顶层 await 的 ESM:不仅当前文件要检查,它导入的依赖也可能让整条模块图变成异步。
这个错误值得单独处理。Node.js 22.12.0、20.19.0 开始默认启用 require(esm),但同步加载仍要求整个依赖图没有顶层 await。旧加载器若只捕获 ERR_REQUIRE_ESM,升级后收到新的错误码,就可能跳过原来的异步导入分支。
先找到实际调用 require 的那一层
记录启动进程的 Node 版本、错误中的模块路径和完整调用栈。编辑器采用的 Node、交互式终端里的 Node,与服务或 CI 真正使用的版本可能不同。报错发生在模块加载阶段,改网络超时或业务重试没有作用。
例如 osls 3.49.0 的插件加载辅助函数,以及 Serverless 3.40.0 对应的辅助函数,只在错误码为 ERR_REQUIRE_ESM 时回退到 import()。如果栈落在这种加载器内,先核对已安装版本的实现;应用入口已经写成 import,也不代表框架内部没有同步加载插件。
顶层 await 不一定就在报错路径对应文件中。继续检查它的静态导入和实际构建产物。函数内部的普通 await 与模块求值时的顶层 await 不同,不能把所有异步函数都当成原因。
Node.js 22.14.0 提供 --experimental-print-required-tla 帮助定位这类顶层等待,但这一版本的诊断会先求值模块,再寻找等待位置。模块初始化可能连接外部服务或写入数据,应在隔离环境中使用,不能当作纯静态检查。
自己维护的加载器,需要把调用链也改为异步
确认该模块确实需要异步初始化后,可用原生动态 import() 加载它。下面是 CommonJS 加载器片段,specifier 应替换为原来已经确认的包名或合法模块 URL:
// plugin-loader.cjs
async function loadPlugin(specifier) {
const namespace = await import(specifier);
return namespace;
}
module.exports = { loadPlugin };
调用方必须等待返回值,不能继续把它当作同步插件对象。import() 返回模块命名空间,默认导出在 namespace.default;原来取命名导出还是默认导出,应按插件实际接口保留,不能一律加 .default。
如果现有框架的插件接口只接受同步结果,应先采用支持该依赖加载方式的框架版本,或按框架的异步初始化入口调整。单独把一行 require 改成 import(),却不改上游调用,容易变成新的 Promise 使用错误。还要看最终 JavaScript 是否保留原生动态导入,避免构建工具又把它转换回同步 require()。
模块初始化本来可以同步完成时,也可以评估去掉顶层等待、把外部资源初始化移到明确的异步函数里。这会改变初始化时序,必须检查谁负责等待、谁处理失败,以及业务是否会在资源准备前启动。
暂时关闭新加载能力,只适用于确认过的版本组合
在 Node.js 20.19.0、22.14.0 这类环境中,--no-experimental-require-module 可以临时关闭 require(esm),让某些旧加载器再次收到 ERR_REQUIRE_ESM 并进入已有回退分支。它不会让同步 require() 获得等待顶层 await 的能力,也不保证所有框架都有回退代码。
这项开关应限定在受影响的启动进程中,并保留已有启动参数。较新 Node 的开关名称还有变化,不能把历史参数复制成所有版本的全局 NODE_OPTIONS。若暂用旧版本组合,也要保留依赖锁文件和运行版本记录,避免下次重建又换回另一套行为。
验收时从原来的服务或 CI 入口启动,确认模块加载和初始化都完成,再调用依赖该插件的功能。只有错误码消失还不够:异步初始化失败被吞掉、插件对象变成 Promise,或导出形状改变,都可能让启动后的行为仍然异常。撤回加载器修改时,应一起恢复与它配套的调用方和运行参数。


