Box ErrorKind inside Error - #31
Conversation
`Error` was 96 bytes, driven by the two three-field `ErrorKind` variants (`WrongDatatype` and `WrongArrayType`, 72 bytes each). It rides in the `Err` arm of every `Result` in the crate, so boxing the kind takes it to 24 bytes; the allocation only happens on the cold error path. `Error::new` boxes for you, so no construction site has to name `Box`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A `Box<ErrorKind>` cannot be pattern-matched through, so the assertions
that used to spell out the whole `Err(Error { .. })` pattern now check
`record_type` separately and match the dereferenced kind.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Post-merge review, as part of reviewing the 0.6 stack for #30. Clippy and tests are clean at the tip of release-0.6. 96 → 24 bytes for a type that rides in every Err arm is worth the break, and the compile-time size assert is the right regression test.
Error::source() is still None, and the From<Error> for ArrowError doc claims otherwise. kind has no #[source], so the chain stops dead at Error. Verified at the tip of release-0.6 with a try_newtype_datatype! failure: err.source().is_none() is true, and the NotEven cause that ErrorKind::Conversion carefully stores as #[source] Box<dyn Error + Send + Sync> is unreachable programmatically — only its Display text survives, twice, through ArrowError::ExternalError. Meanwhile the doc says "The error is preserved (including its source chain)". #[source] pub kind: Box<ErrorKind> fixes the gap and the doc in one line (thiserror allows a field to be both interpolated and the source).
Box is now part of the public API. The break was unavoidable; the box being permanent wasn't. A private field with kind(&self) -> &ErrorKind / into_kind(self) gets the same 24 bytes, reads better at match sites, and lets the representation change again without a third breaking release. You were already spending the break. It would also collapse the tests, where matches!(*err.kind, …) now runs past 120 characters at ~15 call sites.
— Claude
Related
What
Errorwas 96 bytes. It rides in theErrarm of everyResultin the crate, and embedders assert on the size of error enums that hold it.kindis now aBox<ErrorKind>, takingErrorto 24 bytes. The allocation only happens on the cold error path.The two three-field variants set the old size:
WrongDatatypeandWrongArrayTypeeach holdcolumn: String+expected: String+actual: DataType= 72 bytes, plus the discriminant. Arrow'sDataTypeis only 24 bytes, so it was never the culprit.Error::new(record_type, kind)boxes for you, so no construction site namesBox.ErrorKindand all its variants are unchanged.const _: () = assert!(size_of::<Error>() <= 24, …)keeps it from growing back.Breaking:
Error { kind }can no longer be pattern-matched through. Match*err.kind(or&*err.kind) instead, as the tests here now do.Testing
Existing tests, updated for the new match shape. The compile-time assert is the regression test; I checked it fires by tightening it to 16.
Compatibility
Breaking for anyone destructuring
Error, which is why it goes into 0.6.🤖 Generated with Claude Code