错误
主线位置:三条主线的失败出口。查询主线上,服务端的
ErrorResponse变成Error::Database,解码失败变成Error::ColumnDecode,fetch_one没拿到行是Error::RowNotFound;连接池主线上,acquire超时是Error::PoolTimedOut。解码类错误怎么产生在 Row 与 FromRow,ErrorResponse怎么从线上读出来在消息与读写缓冲。这一页讲错误的类型结构,以及怎么在代码里区分它们。
flowchart TB errresp["ErrorResponse 消息"] --> notice["Notice 字段表"] notice --> pgerr[PgDatabaseError] pgerr --> boxed["Box dyn DatabaseError"] boxed --> dbvar["Error::Database"] dbvar --> kind["kind / code / constraint"] dbvar --> down["downcast_ref 回 PgDatabaseError"] decodefail["解码失败"] --> cd["Error::ColumnDecode"] pool["acquire 超时"] --> pto["Error::PoolTimedOut"]
本页实验都在 M2 Max、macOS、rustc 1.99.0、Postgres 16.14 上跑,依赖 sqlx = "=0.9.0",对比时另用 sqlx = "=0.8.6"。
Error 枚举
Error 用 thiserror 派生,每个分支的 Display 就是 #[error(...)] 里那句话。它标了 #[non_exhaustive],match 时必须留 _ 分支,以后加分支不算破坏性改动。按来源分组:
| 分支 | Display | 什么时候出现 |
|---|---|---|
Database(Box<dyn DatabaseError>) | error returned from database: {0} | 服务端返回 ErrorResponse |
RowNotFound | no rows returned by a query that expected to return at least one row | fetch_one 没有行 |
ColumnNotFound(String) | no column found for name: {0} | 按列名取,列不存在 |
ColumnIndexOutOfBounds { index, len } | column index out of bounds: the len is {len}, but the index is {index} | 按下标取,越界 |
ColumnDecode { index, source } | error occurred while decoding column {index}: {source} | 类型不兼容、Decode 失败 |
TypeNotFound { type_name } | type named {type_name} not found | 按名字声明的类型在服务端查不到 |
Encode / Decode | error occurred while encoding a value: {0} 等 | 不针对某一列的编解码错误 |
PoolTimedOut | pool timed out while waiting for an open connection | acquire 超过 acquire_timeout |
PoolClosed | attempted to acquire a connection on a closed pool | 池已 close |
Io(io::Error) | error communicating with database: {0} | socket 读写失败 |
Tls | error occurred while attempting to establish a TLS connection: {0} | TLS 握手失败 |
Protocol(String) | encountered unexpected or invalid data: {0} | 收到不认识的消息,一般是驱动 bug 或连接损坏 |
Configuration | error with configuration: {0} | 连接串解析失败 |
其余几个(InvalidArgument、AnyDriverError、WorkerCrashed、Migrate、InvalidSavePointStatement、BeginFailed、隐藏的 ConfigFile)不在本页展开;迁移错误见迁移,事务相关的见事务。
Protocol 一般用 err_protocol! 构造,它在消息后面拼上 ({module_path}:{line}),即 sqlx 源码里的位置,方便报 bug。
ColumnDecode 和 Database 的内层错误都标了 #[source],又已经拼进了 Display。main 返回 anyhow::Result 时,Caused by: 会把内层错误再印一遍。下面是把一个 domain 列读成 Option<Email> 时的输出(原因见类型一页的 derive 实验):
Error: error occurred while decoding column 2: mismatched types; Rust type `core::option::Option<derive_type::Email>` (as SQL type `email`) is not compatible with SQL type `TEXT`
Caused by:
mismatched types; Rust type `core::option::Option<derive_type::Email>` (as SQL type `email`) is not compatible with SQL type `TEXT`
RowNotFound、PoolTimedOut、PoolClosed
这三个不带数据,Debug 和 Display 都很短。src/bin/db_errors.rs 的最后几行用一个 max_connections(1)、acquire_timeout(200ms) 的池:
let e = sqlx::query("select 1 where false").fetch_one(&pool).await.unwrap_err();
println!("RowNotFound {e}");
let held = pool.acquire().await?;
let e = pool.acquire().await.unwrap_err();
println!("PoolTimedOut {e:?} / {e}");
drop(held);
pool.close().await;
let e = pool.acquire().await.unwrap_err();
println!("PoolClosed {e:?} / {e}");
RowNotFound no rows returned by a query that expected to return at least one row
PoolTimedOut PoolTimedOut / pool timed out while waiting for an open connection
PoolClosed PoolClosed / attempted to acquire a connection on a closed pool
只想要「有就要、没有就算」的查询用 fetch_optional,它返回 Ok(None) 而不是 RowNotFound。PoolTimedOut 不说明为什么超时(连接都被占着,还是新建连接一直失败),定位方法在 acquire。
DatabaseError:与数据库无关的那一层
Error::Database 里装的是 Box<dyn DatabaseError>。DatabaseError 是各驱动错误类型的公共接口:
pub trait DatabaseError: 'static + Send + Sync + StdError {
/// The primary, human-readable error message.
fn message(&self) -> &str;
/// The (SQLSTATE) code for the error.
fn code(&self) -> Option<Cow<'_, str>> {
None
}
// ...
fn constraint(&self) -> Option<&str> {
None
}
fn table(&self) -> Option<&str> {
None
}
fn kind(&self) -> ErrorKind;
/// Returns whether the error kind is a violation of a unique/primary key constraint.
fn is_unique_violation(&self) -> bool {
matches!(self.kind(), ErrorKind::UniqueViolation)
}
/// Returns whether the error kind is a violation of a foreign key.
fn is_foreign_key_violation(&self) -> bool {
matches!(self.kind(), ErrorKind::ForeignKeyViolation)
}
/// Returns whether the error kind is a violation of a check.
fn is_check_violation(&self) -> bool {
matches!(self.kind(), ErrorKind::CheckViolation)
}
}
为什么用 trait 对象而不是给 Error 加一个关联类型:Error 是所有驱动、所有 API 共用的一个具体类型,Pool<Postgres> 和 Pool<Sqlite> 返回的是同一个 sqlx::Error,上层代码可以用 ? 一路传,不带数据库泛型。代价是要拿驱动特有的字段时得向下转型。constraint() 和 table() 的文档都写着 Currently only populated by the Postgres driver。
ErrorKind 也是 non_exhaustive,只收「程序可能想处理」的几类约束冲突:
pub enum ErrorKind {
/// Unique/primary key constraint violation.
UniqueViolation,
/// Foreign key constraint violation.
ForeignKeyViolation,
/// Not-null constraint violation.
NotNullViolation,
/// Check constraint violation.
CheckViolation,
/// Exclusion constraint violation.
ExclusionViolation,
/// An unmapped error.
Other,
}
ExclusionViolation 是 0.9 加的(#3918,PR 说明:the enum is marked as non_exhaustive, so adding a variant is backwards compatible);0.8.6 里排他约束冲突是 Other。便捷方法只有三个 is_*,没有 is_not_null_violation 和 is_exclusion_violation,要用 kind() 比较。
PgDatabaseError:ErrorResponse 的字段表
Postgres 驱动的实现是 PgDatabaseError,就是一个 Notice(ErrorResponse 和 NoticeResponse 格式相同)。Notice 解码时只缓存严重级别、message、code 三个必有字段的位置,其他字段每次调用时按字段类型字节在原始缓冲里找。各方法和协议字段的对应:
| 方法 | 字段 | 例子 |
|---|---|---|
code() | C,SQLSTATE | 23505 |
message() | M | duplicate key value violates unique constraint "person_pkey" |
detail() | D | Key (id)=(1) already exists. |
hint() | H | |
position() | P(或内部查询的 p + q) | Original(14),SQL 里第几个字符 |
schema() / table() / column() / data_type() / constraint() | s / t / c / d / n | person、person_pkey |
file() / line() / routine() | F / L / R | nbtinsert.c、666、_bt_check_unique |
kind() 把 SQLSTATE 映射到 ErrorKind,码表在同文件的 error_codes:23505 唯一、23503 外键、23502 非空、23514 检查、23P01 排他,其余一律 Other。
实验:五种约束冲突和一个拼写错误
src/bin/db_errors.rs 对每条出错的 SQL 打印 Display,再向下转型打印字段:
async fn run(pool: &sqlx::PgPool, label: &str, sql: &'static str) {
let err = sqlx::query(sql).execute(pool).await.unwrap_err();
println!("== {label}");
println!("Display {err}");
if let Some(db) = err.as_database_error() {
let pg = db.downcast_ref::<PgDatabaseError>();
println!(
"kind={:?} code={:?} constraint={:?} table={:?}",
db.kind(),
db.code(),
db.constraint(),
db.table()
);
println!(
"detail={:?} position={:?} file={:?} line={:?} routine={:?}",
pg.detail(),
pg.position(),
pg.file(),
pg.line(),
pg.routine()
);
}
}
表结构:person.id 是主键,person.name 是 not null,person.email 的类型是带 check (value like '%@%') 的 domain;post.person_id 引用 person(id);booking 有 exclude using gist (during with &&),事先插了一行 [1,5)。调用:
run(&pool, "unique", "insert into person (id, name) values (1, 'dup')").await;
run(&pool, "foreign key", "insert into post values (1, 99, 't')").await;
run(&pool, "not null", "insert into person (id) values (3)").await;
run(&pool, "check (domain)", "insert into person (id, name, email) values (3, 'c', 'nope')").await;
run(&pool, "exclusion", "insert into booking values (2, '[3,8)')").await;
run(&pool, "syntax", "select id,\n nme\nfrom person").await;
输出(原样):
== unique
Display error returned from database: duplicate key value violates unique constraint "person_pkey" at line 666
kind=UniqueViolation code=Some("23505") constraint=Some("person_pkey") table=Some("person")
detail=Some("Key (id)=(1) already exists.") position=None file=Some("nbtinsert.c") line=Some(666) routine=Some("_bt_check_unique")
== foreign key
Display error returned from database: insert or update on table "post" violates foreign key constraint "post_person_id_fkey" at line 2608
kind=ForeignKeyViolation code=Some("23503") constraint=Some("post_person_id_fkey") table=Some("post")
detail=Some("Key (person_id)=(99) is not present in table \"person\".") position=None file=Some("ri_triggers.c") line=Some(2608) routine=Some("ri_ReportViolation")
== not null
Display error returned from database: null value in column "name" of relation "person" violates not-null constraint at line 2057
kind=NotNullViolation code=Some("23502") constraint=None table=Some("person")
detail=Some("Failing row contains (3, null, null, null, 2026-10-24 06:54:14.633239, null, null).") position=None file=Some("execMain.c") line=Some(2057) routine=Some("ExecConstraints")
== check (domain)
Display error returned from database: value for domain email violates check constraint "email_check" at line 3831
kind=CheckViolation code=Some("23514") constraint=Some("email_check") table=None
detail=None position=None file=Some("execExprInterp.c") line=Some(3831) routine=Some("ExecEvalConstraintCheck")
== exclusion
Display error returned from database: conflicting key value violates exclusion constraint "booking_during_excl" at line 874
kind=ExclusionViolation code=Some("23P01") constraint=Some("booking_during_excl") table=Some("booking")
detail=Some("Key (during)=([3,8)) conflicts with existing key (during)=([1,5)).") position=None file=Some("execIndexing.c") line=Some(874) routine=Some("check_exclusion_or_unique_constraint")
== syntax
Display error returned from database: column "nme" does not exist at line 3729
kind=Other code=Some("42703") constraint=None table=None
detail=None position=Some(Original(14)) file=Some("parse_relation.c") line=Some(3729) routine=Some("errorMissingColumn")
几点观察:
- 主键冲突报的约束名是索引名
person_pkey。constraint()的文档说 If the error was caused by a conflict of a unique index, this will be the index name。 - 非空冲突没有
constraint,只有table;domain 的检查约束反过来,有constraint没有table(domain 不属于哪张表)。按约束名分派错误时,这两种要另外处理。 detail里带着冲突的值(Key (id)=(1)、整行的Failing row contains (...))。把PgDatabaseError的Debug直接打进日志,这些值也一起进去了。
Display 只打 message,再加一个 at line N,detail 和 hint 不在里面。要给人看的错误信息,自己把 detail() 拼上。
syntax 那一条里的 at line 3729,指的是哪一行?上面最后一条 SQL 是三行:select id,、 nme、from person。错在第 2 行的 nme。Display 末尾却是 at line 3729。3729 是什么的行号?position() 的 Original(14) 又指什么?答案就在同一段输出里。
0.9 的「at line N」
impl Display for PgDatabaseError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(self.message())?;
if let Some(line) = self.line() {
write!(f, " at line {line}")?;
}
Ok(())
}
}
self.line() 是 PgDatabaseError::line,读的是 L 字段,文档注释写得很清楚:The line number of the source-code location where this error was reported. 同一个程序(src/bin/db_display.rs,执行上面的 unique 和 syntax 两条 SQL,打印 {err})在 0.8.6 下:
error returned from database: duplicate key value violates unique constraint "person_pkey"
error returned from database: column "nme" does not exist
在 0.9.0 下:
error returned from database: duplicate key value violates unique constraint "person_pkey" at line 666
error returned from database: column "nme" does not exist at line 3729
at line N 来自 #4118,首次发布于 0.9.0,之前 Display 只有 f.write_str(self.message()) 一行。PR 说明:This makes Postgres errors include the line number for easier debugging. This makes the error output similar to what MySQL and SQLite do by default. 还说 While changing the display of an error could in theory break weird workflows, it seems quite unlikely.
它取的是 ErrorResponse 的 L 字段。上面的实验里,所有这类行号都和 file() 配套:nbtinsert.c 第 666 行、parse_relation.c 第 3729 行,是 Postgres 服务端 C 源码里报错的位置,不是你的 SQL 的第几行。SQL 里的位置在 position(),以字符计。
这个改动对代码的影响:如果你拿 err.to_string() 和固定字符串比较、或者按 Display 做日志去重,升级到 0.9 后会多出一个随 Postgres 版本变化的数字。判断错误种类,应该看 code() / kind(),不看 Display。
怎么判断「唯一约束冲突」
最常见的需求:插入时用户名已存在,要返回 409 而不是 500。三种写法,实验里都为真:
let err = sqlx::query("insert into person (id, name) values ($1, $2)")
.bind(1_i64)
.bind("dup")
.execute(&pool)
.await
.unwrap_err();
match &err {
sqlx::Error::Database(db) if db.is_unique_violation() => {
println!("is_unique_violation: constraint {:?}", db.constraint());
}
_ => println!("other: {err}"),
}
let by_code = err.as_database_error().and_then(|db| db.code()).as_deref() == Some("23505");
println!("code == 23505: {by_code}");
println!("kind == UniqueViolation: {}", err.as_database_error().map(|d| d.kind()) == Some(ErrorKind::UniqueViolation));
is_unique_violation: constraint Some("person_pkey")
code == 23505: true
kind == UniqueViolation: true
is_unique_violation()不依赖具体数据库:MySQL 驱动按ER_DUP_ENTRY等错误号、SQLite 驱动按SQLITE_CONSTRAINT_UNIQUE/SQLITE_CONSTRAINT_PRIMARYKEY实现了各自的kind()。code()返回Option<Cow<str>>,是 SQLSTATE 原文;ErrorKind只覆盖五种,其他错误(比如40001序列化失败、40P01死锁)要靠code()判断。- 一张表有多个唯一约束时(主键和
email unique),再看constraint()区分是哪一个。
需要 detail()、position() 这些 Postgres 独有的字段时,用 downcast_ref 转回 PgDatabaseError。它在类型不对时会 panic(文档:This is a deliberate contrast from Error::downcast_ref which returns Option<&E>),不确定驱动时用 try_downcast_ref。
动脑筋答案
syntax 那一条的两行输出:
Display error returned from database: column "nme" does not exist at line 3729
detail=None position=Some(Original(14)) file=Some("parse_relation.c") line=Some(3729) routine=Some("errorMissingColumn")
3729:line()读的是L字段,和file=Some("parse_relation.c")、routine=Some("errorMissingColumn")一起看,是 Postgres 16.14 服务端源码parse_relation.c里errorMissingColumn报错的那一行。换一个 Postgres 版本,这个数字就可能变。Original(14):P字段,原始 SQL 里的字符位置,从 1 数起。select id,\n nme里,s是第 1 个字符,\n是第 11 个,两个空格是 12、13,nme的n是第 14 个。要指出 SQL 里出错的行,得自己用position()去数换行。
小结
sqlx::Error是non_exhaustive的具体枚举,所有驱动共用;数据库报的错装在Error::Database(Box<dyn DatabaseError>)里,其余分支是 sqlx 自己产生的。DatabaseError给出message、code(SQLSTATE)、constraint、table和kind;ErrorKind只有唯一、外键、非空、检查、排他(0.9 新增)五种加Other。- 判断唯一约束冲突:
is_unique_violation()、code() == "23505"或kind() == ErrorKind::UniqueViolation;多个唯一约束时再看constraint(),主键冲突报的是索引名。 PgDatabaseError是ErrorResponse的字段表,detail、position等要downcast_ref后取;detail里带着冲突的值。- 0.9 起
Display末尾多了at line N,它是服务端 C 源码的行号(L字段),不是 SQL 的行;SQL 里的位置看position()。不要按Display文本判断错误种类。