neuer_error/
error.rs

1//! Error type implementation.
2
3use ::alloc::{borrow::Cow, boxed::Box, vec, vec::Vec};
4use ::core::{
5	any::Any,
6	error::Error,
7	fmt::{Debug, Display, Formatter, Result as FmtResult},
8	panic::Location,
9};
10
11use crate::features::{AnyDebugSendSync, ErrorSendSync};
12
13/// Error information for humans.
14/// Error message with location information.
15#[derive(Debug)]
16pub(crate) struct HumanInfo {
17	/// Message text.
18	pub(crate) message: Cow<'static, str>,
19	/// Location of occurrence.
20	pub(crate) location: &'static Location<'static>,
21}
22
23/// Error information for machines.
24/// Arbitrary, project specific types of information.
25#[derive(Debug)]
26pub(crate) struct MachineInfo {
27	/// Attachment.
28	pub(crate) attachment: Box<dyn AnyDebugSendSync>,
29}
30
31/// Context information, either machine or human.
32/// Joined in a union type to save the space of another `Vec` in the error type.
33#[derive(Debug)]
34pub(crate) enum Info {
35	/// Contextual information for humans.
36	Human(HumanInfo),
37	/// Contextual information for machines.
38	Machine(MachineInfo),
39}
40// Ensure niche-optimization is active.
41const _: () = {
42	assert!(size_of::<Info>() == size_of::<HumanInfo>());
43};
44
45/// Generic rich error type for use within `Result`s, for libraries and applications.
46///
47/// Add human context information, including code locations, via the `context` method.
48/// Attach machine context information via the `attach` and `attach_override` methods.
49///
50/// ## Error Formatting
51///
52/// The normal `Debug` implementation (`"{err:?}"`) will print the error with multi-line formatting,
53/// exactly how `Display` is doing it. The alternate `Debug` implementation (`"{err:#?}"`) will show
54/// the pretty-printed usual debug representation of the internal types.
55///
56/// When using the `Display` implementation, the normal implementation (`"{err}"`) will use
57/// multi-line formatting. You can use the alternate format (`{err:#}`) to get a compact single-line
58/// version. instead of multi-line formatted.
59#[derive(Default)]
60pub struct CtxError(CtxErrorImpl);
61
62/// Inner implementation of [`CtxError`] that implements [`Error`].
63#[derive(Default)]
64pub struct CtxErrorImpl {
65	/// Contextual error information.
66	infos: Vec<Info>,
67	/// Source error.
68	source: Option<Box<dyn ErrorSendSync>>,
69}
70
71impl Debug for CtxError {
72	fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
73		Debug::fmt(&self.0, f)
74	}
75}
76
77impl Display for CtxError {
78	fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
79		Display::fmt(&self.0, f)
80	}
81}
82
83impl Debug for CtxErrorImpl {
84	fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
85		if f.alternate() {
86			f.debug_struct("CtxError")
87				.field("infos", &self.infos)
88				.field("source", &self.source)
89				.finish()
90		} else {
91			Display::fmt(self, f)
92		}
93	}
94}
95
96impl Display for CtxErrorImpl {
97	fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
98		let mut human = self.contexts().peekable();
99		if human.peek().is_none() {
100			write!(f, "Unknown error")?;
101		}
102		while let Some(context) = human.next() {
103			if f.alternate() {
104				write!(f, "{} (at {})", context.message, context.location)?;
105				if human.peek().is_some() {
106					write!(f, "; ")?;
107				}
108			} else {
109				writeln!(f, "{}", context.message)?;
110				write!(f, "|- at {}", context.location)?;
111				if human.peek().is_some() {
112					writeln!(f)?;
113					writeln!(f, "|")?;
114				}
115			}
116		}
117
118		#[expect(trivial_casts, reason = "Not that trivial as it seems? False positive")]
119		let mut source = self.source.as_deref().map(|e| e as &(dyn Error + 'static));
120		while let Some(err) = source {
121			if f.alternate() {
122				write!(f, "; caused by: {err}")?;
123			} else {
124				writeln!(f)?;
125				writeln!(f, "|")?;
126				write!(f, "|- caused by: {err}")?;
127			}
128
129			source = err.source();
130		}
131
132		Ok(())
133	}
134}
135
136impl CtxError {
137	/// Create new error.
138	#[track_caller]
139	#[must_use]
140	#[inline]
141	pub fn new<C>(context: C) -> Self
142	where
143		C: Into<Cow<'static, str>>,
144	{
145		let infos =
146			vec![Info::Human(HumanInfo { message: context.into(), location: Location::caller() })];
147		Self(CtxErrorImpl { infos, ..Default::default() })
148	}
149
150	/// Create new error from source error.
151	#[track_caller]
152	#[must_use]
153	#[inline]
154	pub fn new_with_source<C, E>(context: C, source: E) -> Self
155	where
156		C: Into<Cow<'static, str>>,
157		E: ErrorSendSync + 'static,
158	{
159		let infos =
160			vec![Info::Human(HumanInfo { message: context.into(), location: Location::caller() })];
161		Self(CtxErrorImpl { infos, source: Some(Box::new(source)) })
162	}
163
164	/// Convert source error.
165	#[must_use]
166	#[inline]
167	pub fn from_source<E>(source: E) -> Self
168	where
169		E: ErrorSendSync + 'static,
170	{
171		Self(CtxErrorImpl { source: Some(Box::new(source)), ..Default::default() })
172	}
173
174	/// Add human context to the error.
175	#[track_caller]
176	#[must_use]
177	#[inline]
178	pub fn context<C>(self, context: C) -> Self
179	where
180		C: Into<Cow<'static, str>>,
181	{
182		Self(self.0.context(context))
183	}
184
185	/// Add machine context to the error.
186	///
187	/// This will not override existing attachments. If you want to replace and override any
188	/// existing attachments of the same type, use `attach_override` instead.
189	#[must_use]
190	#[inline]
191	pub fn attach<C>(self, context: C) -> Self
192	where
193		C: AnyDebugSendSync + 'static,
194	{
195		Self(self.0.attach(context))
196	}
197
198	/// Set machine context in the error.
199	///
200	/// This will override existing attachments of the same type. If you want to add attachments of
201	/// the same type, use `attach` instead.
202	#[must_use]
203	pub fn attach_override<C>(self, context: C) -> Self
204	where
205		C: AnyDebugSendSync + 'static,
206	{
207		Self(self.0.attach_override(context))
208	}
209
210	/// Get an iterator over the human context infos.
211	#[inline]
212	#[cfg_attr(not(test), expect(unused, reason = "For consistency"))]
213	pub(crate) fn contexts(&self) -> impl Iterator<Item = &'_ HumanInfo> {
214		self.0.contexts()
215	}
216
217	/// Get an iterator over the machine context attachments of the given type.
218	#[inline]
219	pub fn attachments<C>(&self) -> impl Iterator<Item = &'_ C>
220	where
221		C: AnyDebugSendSync + 'static,
222	{
223		self.0.attachments()
224	}
225
226	/// Get the machine context attachment of the given type.
227	#[must_use]
228	#[inline]
229	pub fn attachment<C>(&self) -> Option<&C>
230	where
231		C: AnyDebugSendSync + 'static,
232	{
233		self.0.attachment()
234	}
235
236	/// Get the source error.
237	#[must_use]
238	#[inline]
239	pub fn source(&self) -> Option<&(dyn ErrorSendSync + 'static)> {
240		self.0.source.as_deref()
241	}
242
243	/// Unwrap this error into a [`CtxErrorImpl`] that implements [`Error`]. Note however, that it
244	/// does not offer all of the functionality and might be unwieldy for other general purposes
245	/// than interfacing with other error types.
246	#[must_use]
247	#[inline]
248	pub fn into_error(self) -> CtxErrorImpl {
249		self.0
250	}
251}
252
253impl CtxErrorImpl {
254	/// Wrap this error back into a [`CtxError`] that offers all of the functionality.
255	#[must_use]
256	#[inline]
257	pub const fn wrap(self) -> CtxError {
258		CtxError(self)
259	}
260
261	/// Add human context to the error.
262	#[track_caller]
263	#[must_use]
264	#[inline]
265	pub fn context<C>(mut self, context: C) -> Self
266	where
267		C: Into<Cow<'static, str>>,
268	{
269		let context = HumanInfo { message: context.into(), location: Location::caller() };
270		self.infos.push(Info::Human(context));
271		self
272	}
273
274	/// Add machine context to the error.
275	///
276	/// This will not override existing attachments. If you want to replace and override any
277	/// existing attachments of the same type, use `attach_override` instead.
278	#[must_use]
279	#[inline]
280	pub fn attach<C>(mut self, context: C) -> Self
281	where
282		C: AnyDebugSendSync + 'static,
283	{
284		let context = MachineInfo { attachment: Box::new(context) };
285		self.infos.push(Info::Machine(context));
286		self
287	}
288
289	/// Set machine context in the error.
290	///
291	/// This will override existing attachments of the same type. If you want to add attachments of
292	/// the same type, use `attach` instead.
293	#[must_use]
294	pub fn attach_override<C>(mut self, mut context: C) -> Self
295	where
296		C: AnyDebugSendSync + 'static,
297	{
298		let mut inserted = false;
299		#[expect(trivial_casts, reason = "Not that trivial as it seems? False positive")]
300		self.infos.retain_mut(|info| match info {
301			Info::Machine(ctx) => {
302				if let Some(content) =
303					(ctx.attachment.as_mut() as &mut (dyn Any + 'static)).downcast_mut::<C>()
304				{
305					if !inserted {
306						core::mem::swap(content, &mut context);
307						inserted = true;
308						true // First attachment of same type, was replaced with new value, so keep it.
309					} else {
310						false // Another attachment of the same type, remove duplicate.
311					}
312				} else {
313					true // Attachment of different type.
314				}
315			}
316			_ => true,
317		});
318		if !inserted {
319			// No existing attachment of the same type was found to be replaced, so add a new one.
320			self.infos.push(Info::Machine(MachineInfo { attachment: Box::new(context) }));
321		}
322		self
323	}
324
325	/// Get an iterator over all context infos.
326	#[inline]
327	pub(crate) fn infos(&self) -> impl Iterator<Item = &'_ Info> {
328		self.infos.iter().rev()
329	}
330
331	/// Get an iterator over the human context infos.
332	#[inline]
333	pub(crate) fn contexts(&self) -> impl Iterator<Item = &'_ HumanInfo> {
334		self.infos().filter_map(|info| match info {
335			Info::Human(info) => Some(info),
336			_ => None,
337		})
338	}
339
340	/// Get an iterator over the machine context attachments of the given type.
341	#[inline]
342	pub fn attachments<C>(&self) -> impl Iterator<Item = &'_ C>
343	where
344		C: AnyDebugSendSync + 'static,
345	{
346		#[expect(trivial_casts, reason = "Not that trivial as it seems? False positive")]
347		self.infos()
348			.filter_map(|info| match info {
349				Info::Machine(info) => Some(info),
350				_ => None,
351			}) // Catch the newest attachment first.
352			.map(|ctx| ctx.attachment.as_ref() as &(dyn Any + 'static))
353			.filter_map(|ctx| ctx.downcast_ref())
354	}
355
356	/// Get the machine context attachment of the given type.
357	#[must_use]
358	#[inline]
359	pub fn attachment<C>(&self) -> Option<&C>
360	where
361		C: AnyDebugSendSync + 'static,
362	{
363		self.attachments().next()
364	}
365}
366
367impl From<CtxError> for CtxErrorImpl {
368	#[inline]
369	fn from(err: CtxError) -> Self {
370		err.0
371	}
372}
373
374#[diagnostic::do_not_recommend]
375impl<E> From<E> for CtxError
376where
377	E: ErrorSendSync + 'static,
378{
379	#[inline]
380	fn from(err: E) -> Self {
381		Self::from_source(err)
382	}
383}
384
385impl Error for CtxErrorImpl {
386	#[inline]
387	fn source(&self) -> Option<&(dyn Error + 'static)> {
388		#[expect(trivial_casts, reason = "Not that trivial as it seems? False positive")]
389		self.source.as_deref().map(|e| e as &(dyn Error + 'static))
390	}
391}
392
393impl AsRef<dyn Error> for CtxError {
394	#[inline]
395	fn as_ref(&self) -> &(dyn Error + 'static) {
396		&self.0
397	}
398}
399
400#[cfg(feature = "send")]
401impl AsRef<dyn Error + Send> for CtxError {
402	#[inline]
403	fn as_ref(&self) -> &(dyn Error + Send + 'static) {
404		&self.0
405	}
406}
407
408#[cfg(all(feature = "send", feature = "sync"))]
409impl AsRef<dyn Error + Send + Sync> for CtxError {
410	#[inline]
411	fn as_ref(&self) -> &(dyn Error + Send + Sync + 'static) {
412		&self.0
413	}
414}
415
416impl From<CtxError> for Box<dyn Error> {
417	#[inline]
418	fn from(this: CtxError) -> Self {
419		Box::new(this.into_error())
420	}
421}
422
423#[cfg(feature = "send")]
424impl From<CtxError> for Box<dyn Error + Send> {
425	#[inline]
426	fn from(this: CtxError) -> Self {
427		Box::new(this.into_error())
428	}
429}
430
431#[cfg(all(feature = "send", feature = "sync"))]
432impl From<CtxError> for Box<dyn Error + Send + Sync> {
433	#[inline]
434	fn from(this: CtxError) -> Self {
435		Box::new(this.into_error())
436	}
437}
438
439
440#[cfg(feature = "std")]
441impl std::process::Termination for CtxError {
442	#[inline]
443	fn report(self) -> std::process::ExitCode {
444		std::process::Termination::report(self.0)
445	}
446}
447
448#[cfg(feature = "std")]
449impl std::process::Termination for CtxErrorImpl {
450	#[inline]
451	fn report(self) -> std::process::ExitCode {
452		self.attachment::<std::process::ExitCode>()
453			.copied()
454			.unwrap_or(std::process::ExitCode::FAILURE)
455	}
456}