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()”最省心。
