Node 内置 SQLite 报 statement has been finalized 的原因

Node 22 开始有了内置的 node:sqlite,同步 API 用起来非常顺。但启动时挂:

ExperimentalWarning: SQLite is an experimental feature
Failed to start server Error: statement has been finalized
    at readDeviceStates (src/lib/database.ts:76:38)
    ...
  code: 'ERR_INVALID_STATE'

这个错的字面意思很明确:你在一个已经 finalize() 过的 prepared statement 上继续调用 all() / get() / iterate()

三种最常见的成因

1. 手动过早 finalize

const stmt = db.prepare("SELECT * FROM devices");
try {
  return stmt.all();
} finally {
  stmt.finalize();
}

看上去正确。但如果 stmt.all() 返回的是迭代器iterate())而不是数组(all()),外层还在懒消费,finalize 一走就崩。用 all() 拿到具体数组的写法没这个问题。

2. 用了 using 自动 finalize(Node 22 的新语法)

export function readDeviceStates() {
  using stmt = db.prepare("SELECT * FROM device_states");
  return stmt.iterate();      // ← 迭代器,出了作用域 stmt 就被 finalize 了
}

using 借用了 TC39 的 Explicit Resource Management 提案,出作用域时会自动调 [Symbol.dispose](),也就是 finalize()。返回迭代器给外部继续用,外部一 next() 就炸。

规则using + iterate() 不能混,要么用 all() 一次拿全部再返回,要么调用方也 using

3. 模块级缓存了 stmt

// 模块顶部
const readStmt = db.prepare("SELECT * FROM device_states");

export function readDeviceStates() {
  return readStmt.all();
}

看着挺合理——避免每次 prepare 的开销。但代码其它地方可能:

  • 单元测试用 db.close() 关掉了数据库(stmt 一起 finalize)
  • 有热重载/HMR 重新 import 了模块(旧 stmt 引用还留着)
  • 显式调用了 readStmt.finalize() 想清理,然后忘了

之后再走这个函数就 ERR_INVALID_STATE

推荐写法

每次现 prepare,让 GC 管理

export function readDeviceStates() {
  const stmt = db.prepare("SELECT * FROM device_states");
  return stmt.all();       // 立刻取完,不返回 iterator
}

现代 SQLite 的 prepare 开销很小,不需要为性能提前优化。

要缓存的话,配套复用而不是复用 + finalize

const cache = new Map<string, ReturnType<typeof db.prepare>>();

function s(sql: string) {
  let stmt = cache.get(sql);
  if (!stmt) {
    stmt = db.prepare(sql);
    cache.set(sql, stmt);
  }
  return stmt;
}

// 用:
s("SELECT * FROM device_states").all();

不在缓存生命周期外手动 finalize。数据库 close 时统一 cache.clear()

要用 using 就一次性拿完数据

export function readDeviceStates() {
  using stmt = db.prepare("SELECT * FROM device_states");
  return stmt.all();       // 数组,可以安全跨作用域
}

一句话总结

statement has been finalized = 你正在用一个死掉的 stmt。别返回迭代器给作用域外别在模块级缓存又手动 finalize。改成”每次 prepare + all()”最省心。