Error design first answers two questions: what should the caller act upon, and what information does the troubleshooter need to understand the failure? Specific enums are suitable for expressing matchable categories; context explains which business step was being processed at the time. The two can coexist; there is no need to treat "recoverable" and "reportable" as mutually exclusive choices.
Error provides a common error interface
std::error::Error requires Debug and Display, and can expose another error that caused the current failure via source(). Display is for human-readable descriptions, Debug is for debugging representations; source represents structured causal relationships, not concatenating multiple strings into a single paragraph. Error
The error cause chain may have only one layer, or it may span parsing, storage, and application layers. Backtraces require collecting stack information at a specific point in time; traversing source will not automatically derive the function call stack. Even if a stack is available, the capture location may not be where the failure originally occurred.
You can implement the cause chain completely without derive macros
First, look at the standard library version to see exactly what thiserror generates later. Field names identify the failing configuration, source preserves the original parsing error, and Display is only responsible for the explanation at the current layer.
usestd::{error::Error, fmt,num::ParseIntError};#[derive(Debug)]structFieldError{field:&'staticstr,
source: ParseIntError,
}implfmt::Display forFieldError{fnfmt(&self, f:&mutfmt::Formatter<'_>)->fmt::Result{write!(f,"Configuration field {} is not a valid u16",self.field)}}implError forFieldError{fnsource(&self)->Option<&(dyn Error + 'static)>{Some(&self.source)}}fnparse(field:&'staticstr, text:&str)->Result<u16, FieldError>{
text.parse().map_err(|source|FieldError { field, source })}fnmain(){let error =parse("port","bad").unwrap_err();assert_eq!(error.field,"port");assert!(error.to_string().contains("port"));let source = error.source().unwrap();assert!(source.downcast_ref::<ParseIntError>().is_some());assert!(source.source().is_none());}
source returns a borrow; the reference cannot outlive the current error object. The 'static constraint in the return type targets the erased cause type, not extending that reference into a static reference. The Error trait itself does not require all implementations to be Send, Sync, or 'static; these are often requirements imposed by inter-thread propagation and specific error containers.
If the upper-level Display has already embedded all source text, reporters printing layer by layer may repeat the same cause. Let each layer describe only the new context, and let a unified reporter expand the cause chain; if the interface chooses to display a single complete line, the overall strategy should remain consistent.
Does wrapping preserve the cause type?
Identical display text may come from very different structures: a chain preserving ParseIntError, or a String alone. Inspect the outer and inner layers to see that plain dyn Error downcasting tests the current object. Display text, causal structure and a captured stack are distinct information.
Preparing the visual
Does wrapping preserve the cause type?
source preserves a traversable cause object; to_string retains display text, not its original type.
{"id":"rust-error-chain","title":"Does wrapping preserve the cause type?","summary":"source preserves a traversable cause object; to_string retains display text, not its original type.","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><label class=\"check\"><input type=\"checkbox\" id=\"preserve\" checked><span data-i18n=\"preserve\"></span></label><div class=\"presets\" role=\"group\" data-i18n-label=\"mode\"><button data-mode=\"outer\" data-i18n=\"outer\"></button><button data-mode=\"inner\" data-i18n=\"inner\"></button></div><section class=\"panel\"><h3 data-i18n=\"chain\"></h3><div id=\"chain\"></div></section><div class=\"metrics\"><div><span data-i18n=\"object\"></span><strong id=\"object\"></strong></div><div><span data-i18n=\"downcast\"></span><strong id=\"downcast\"></strong></div></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"kept\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"lost\"></p></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:550;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:550 20px ui-monospace,monospace}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:ui-monospace,monospace;font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px ui-monospace,monospace;background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 ui-monospace,monospace;white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px ui-monospace,monospace}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 ui-monospace,monospace;white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:ui-monospace,monospace}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n.budget{display:flex;height:28px;margin:16px 0;background:var(--surface);border-radius:5px;overflow:hidden}.budget i{display:block;background:var(--accent);transition:width .2s}.budget i.gold{background:var(--second)}.intervals{margin:16px 0}.interval{position:relative;height:36px;background:var(--surface);margin:6px 0;border-radius:4px;overflow:hidden}.interval i{position:absolute;height:100%;background:var(--accent-soft);border-left:2px solid var(--accent)}.interval i.gold{background:var(--second-soft);border-color:var(--second)}.interval span{position:relative;z-index:1;font:12px/36px ui-monospace,monospace;padding-left:7px}.table .tr>*{min-height:4.8em}.formula{white-space:pre-line;min-height:7.2em}.actions:has(>button:only-child){grid-template-columns:1fr}.metrics strong{min-height:3.2em}.rangegrid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:3px;margin:14px 0}.rangegrid .cell{min-height:30px;padding:3px 1px;font-size:10px}.message{min-height:7.2em;white-space:pre-line;font:13px/1.8 ui-monospace,monospace;overflow-wrap:anywhere}\n/* Allow CJK hanging punctuation without overflowing a metric's border box. */\n.metrics>div{padding-right:4px}.metrics span{line-break:anywhere}\n\n.chart-wrap{margin:18px 0}.chart-label{font-size:12px;color:var(--muted);min-height:3.2em}.curve{display:block;width:100%;height:auto;max-height:320px;overflow:visible}.curve text{font:11px ui-monospace,monospace;fill:var(--muted)}.gridline{stroke:var(--rule);stroke-width:1}.metrics strong{font-size:20px;min-height:3.2em}.formula{font:12px/1.8 ui-monospace,monospace;min-height:9em}.readout{min-height:8em}.presets button{flex-basis:30%}.scope p{overflow-wrap:anywhere} .unit-note{font-size:12px;color:var(--muted);margin:10px 0}\n\n.metrics span{word-break:break-all;line-break:strict}.metrics>div{padding-right:6px}\n\n.cell{min-height:72px}.metrics strong{min-height:3.6em}\n\n.metrics strong{font-size:14px;min-height:5em}#chain{min-height:5.4em;font:14px/1.8 ui-monospace,monospace;overflow-wrap:anywhere}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).classList.add('table');$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';row.style.gridTemplateColumns='repeat('+r.length+',minmax(0,1fr))';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}function band(id,parts,total){$('#'+id).replaceChildren(...parts.map((p,i)=>{const e=document.createElement('i');e.style.width=(100*p.value/total)+'%';e.className=i%2?'gold':'on';e.title=p.name+': '+p.value;return e}));}function intervals(id,items,total){$('#'+id).replaceChildren(...items.map(p=>{const row=document.createElement('div');row.className='interval';const bar=document.createElement('i');bar.style.left=(100*p.start/total)+'%';bar.style.width=(100*(p.end-p.start)/total)+'%';bar.className=p.cls||'';const label=document.createElement('span');label.textContent=p.label;row.append(bar,label);return row;}));}function plot(id,series,xr,yr,opts={}){const e=$('#'+id),X=x=>42+(x-xr[0])/(xr[1]-xr[0])*298,Y=y=>175-(y-yr[0])/(yr[1]-yr[0])*148;let out='<defs><clipPath id=\"clip-'+id+'\"><rect x=\"42\" y=\"27\" width=\"298\" height=\"148\"/></clipPath></defs>';for(let i=0;i<3;i++){let x=xr[0]+i*(xr[1]-xr[0])/2,y=yr[0]+i*(yr[1]-yr[0])/2;out+='<path class=\"gridline\" d=\"M '+X(x)+' 27V175M42 '+Y(y)+'H340\"/><text x=\"'+X(x)+'\" y=\"194\" text-anchor=\"middle\">'+esc(opts.xfmt?opts.xfmt(x):Number(x.toFixed(2)))+'</text><text x=\"36\" y=\"'+(Y(y)+4)+'\" text-anchor=\"end\">'+esc(opts.yfmt?opts.yfmt(y):Number(y.toFixed(2)))+'</text>';}out+='<g clip-path=\"url(#clip-'+id+')\">';for(const s of series){out+='<path fill=\"none\" stroke=\"'+(s.color||'var(--accent)')+'\" stroke-width=\"2.5\" '+(s.dash?'stroke-dasharray=\"5 4\"':'')+' d=\"'+s.pts.map((p,i)=>(i?'L':'M')+X(p[0]).toFixed(2)+','+Y(p[1]).toFixed(2)).join(' ')+'\"/>';}for(const p of opts.points||[])out+='<circle cx=\"'+X(p[0])+'\" cy=\"'+Y(p[1])+'\" r=\"4\" fill=\"var(--second)\" stroke=\"var(--paper)\" stroke-width=\"1.5\"/>';if(opts.cursor!==undefined)out+='<path d=\"M'+X(opts.cursor)+' 27V175\" stroke=\"var(--muted)\" stroke-dasharray=\"3 3\"/>';e.innerHTML=out+'</g>';e.dataset.curves=JSON.stringify(series.map(s=>s.pts));}const curve=(fn,lo,hi,n=120)=>Array.from({length:n+1},(_,i)=>{const x=lo+(hi-lo)*i/n;return[x,fn(x)]});function inputs(draw){document.querySelectorAll('input,select').forEach(e=>{e.addEventListener('input',draw);e.addEventListener('change',draw)});draw()}const n=id=>+$('#'+id).value;const show=(id,v,d=3)=>set(id,Number(v.toFixed(d)));let mode='outer';function draw(){let p=$('#preserve').checked;pressed(mode);set('chain',p?'ConfigError\\n source() → ParseIntError\\n source() → None':'ConfigError { message: String }\\n source() → None');set('object',mode==='outer'?'ConfigError':p?'ParseIntError':'—');set('downcast',mode==='inner'&&p?'Some(&error)':'None');set('explanation',t(p?'kept':'lost'));}document.querySelectorAll('[data-mode]').forEach(b=>b.onclick=()=>{mode=b.dataset.mode;draw()});inputs(draw);","audio":false,"strings":{"chain":"Traversable relationship","downcast":"downcast_ref::<ParseIntError>()","heading":"Does wrapping preserve the cause type?","inner":"Underlying cause","intro":"Choose source preservation or string-only storage, then inspect a layer.","kept":"The cause remains, but plain dyn Error downcasting checks only this object; use source() to reach the inner cause.","lost":"The string can display the same message, but no ParseIntError object remains to traverse or identify.","mode":"Inspect object","object":"Type at this position","outer":"Application error","preserve":"Preserve structured source","reset":"Reset","scope":"A plain std::error::Error chain, not anyhow’s extra context downcasting or a call stack.","scopeLabel":"Model scope and assumptions"}}
Complete example: types for decisions, context for location
Cargo.toml:
[package]
name = "error-demo"
version = "0.1.0"
edition = "2024"
[dependencies]
thiserror = "2"
anyhow = "1"
src/main.rs:
useanyhow::Context;usestd::num::ParseIntError;usethiserror::Error;#[derive(Debug, Error)]enumPortError{#[error("port is not a valid u16")]
Invalid(#[from] ParseIntError),#[error("port zero is disabled by this application")]
Zero,}fnparse_port(text:&str)->Result<u16, PortError>{let port = text.parse::<u16>()?;if port ==0{returnErr(PortError::Zero);}Ok(port)}fnstartup(text:&str)->anyhow::Result<u16>{parse_port(text).context("cannot configure listening port")}fnmain(){assert_eq!(startup("8080").unwrap(),8080);assert!(matches!(parse_port("0"),Err(PortError::Zero)));assert!(matches!(parse_port("70000"),Err(PortError::Invalid(_))));let error =startup("bad").unwrap_err();assert!(matches!(error.downcast_ref::<PortError>(),Some(PortError::Invalid(_))));for(level, cause)in error.chain().enumerate(){eprintln!("{level}: {cause}");}assert_eq!(error.chain().count(),3);}
Here, prohibiting port 0 is an application policy, not a rule of u16 or network protocols. Underlying parsing failures are converted to Invalid via #[from]; business rejections directly construct Zero. After adding startup context at the application outer layer, PortError can still be found via downcast_ref.
thiserror generates standard trait implementations
thiserror helps generate Display, Error, and specified From implementations; the exposed errors are still their own types. #[from] simultaneously treats that field as source; since From only accepts the source error, variants cannot also require an arbitrary path or business ID field (the documentation separately allows corresponding backtrace fields). thiserror
If an error needs to carry a path, construct a variant containing path and source using map_err at the call site, preserving the original error as source, rather than storing only error.to_string(). The former preserves the cause type; the latter leaves only display text.
#[error(transparent)] is used to transparently delegate display and source behavior to the inner error, without adding a layer of business explanation out of thin air. When you need to explain "which configuration failed to read," you should explicitly model this layer of context.
The same underlying error may come from different business steps
From can only convert based on the incoming error value and cannot know whether it is reading configuration or writing a checkpoint. If multiple steps produce io::Error, add operation context at the point of occurrence to avoid conflicts from writing two identical #[from] variants.
The following example uses the thiserror dependency above; it demonstrates construction by injecting a failing operation, without actually reading or writing files.
Here, resource stores a resource identifier suitable for reporting, and does not need to contain the full input content. #[source] preserves the error cause, and map_err fills in context at the call site that From does not have. The number of error wrapping layers should correspond to valuable new information, rather than wrapping every layer with synonymous explanations as it passes through helper functions.
anyhow wraps errors, but does not define recovery strategies for you
anyhow::Error can accept concrete errors that satisfy Error + Send + Sync + 'static. context adds explanation, with_context allows constructing expensive context only on failure; for errors retained in the wrapper, type downcasting is still possible. anyhow
Converting all errors to strings and deciding to retry based on whether the string contains "timeout" binds business behavior to unstable text. When distinguishing between rate limiting, authentication failure, and temporary unavailability, even within the application, you should retain specific categories or stable error codes.
anyhow does not automatically catch panics, does not automatically write logs, and does not roll back side effects. It makes error propagation and reporting more convenient; recovery, retry, and transactions remain the program's own strategies.
Context can be readable or identifiable types
The following still uses the same Cargo dependencies. The context LoadingConfig is responsible for identifying the business step, the underlying ParseIntError preserves the specific failure, and both can be found via anyhow's downcast_ref.
useanyhow::Context;usestd::{fmt,num::ParseIntError};#[derive(Debug)]structLoadingConfig;implfmt::Display forLoadingConfig{fnfmt(&self, f:&mutfmt::Formatter<'_>)->fmt::Result{
f.write_str("Failed to load listening configuration")}}fnmain(){let error ="bad".parse::<u16>().context(LoadingConfig).unwrap_err();assert!(error.downcast_ref::<LoadingConfig>().is_some());assert!(error.downcast_ref::<ParseIntError>().is_some());assert_eq!(error.to_string(),"Failed to load listening configuration");assert_eq!(error.chain().count(),2);}
This uses anyhow's specific support for its context wrapper; you cannot infer that downcast on arbitrary Box<dyn Error> will automatically traverse the source chain. downcast checks the concrete type of the current object on standard trait objects; when you need to check causes, you should explicitly advance along source. anyhow Context and downcast
The 'static constraint does not mean the error must leak or live until process exit. Errors holding String can satisfy 'static and be freed quickly; errors holding temporary input &str may not fit into containers requiring 'static. When you need to retain location information across tasks, you can save owned short identifiers, ranges, or line numbers.
Division of labor: cause chain, context, backtrace
Functions and locations, affected by symbol information
anyhow's stack trace support is affected by build and runtime configurations. In typical std builds, you can request error backtraces using RUST_LIB_BACKTRACE=1; RUST_BACKTRACE also involves panic backtraces. Do not treat "having a source chain" as "definitely having a complete stack."
From error category to action, one business judgment is still missing
Category
Possible Action
Conditions Still to Confirm
Configuration Syntax Error
Point out the field and stop startup
Do not use default values to mask explicit errors
Insufficient Permissions
Correct permissions or identity
Retrying the same request usually does not change permissions
Request Timeout
Query results or limited retry
The original operation may have already succeeded; consider idempotency
Rate Limiting
Wait according to service guidelines
Retry budget, deadline, and request validity
Resource Not Found
Return empty result or report failure
Whether the resource is inherently optional
Error does not define is_retryable because "whether to retry" usually depends on operation semantics. Even if the error comes from a temporary network failure, a transfer request cannot be resent solely based on a transient flag; if the response is lost, the server may have already executed successfully. Types provide the basis for decision-making; retry protocols still need to include constraints on count, time, and idempotency.
Trade-offs at interface boundaries
Libraries providing matchable error types are usually beneficial for caller recovery, and thiserror can reduce implementation code; the application top layer uniformly carrying context is usually suitable for anyhow. They are common divisions of labor, not rules that "libraries must use a specific crate, and applications do not need to distinguish errors."
Variants of public error enums are also part of compatibility design; consider non_exhaustive as needed for extension. Error messages should be sufficient to locate the problem, but avoid recording passwords or full credentials verbatim. Usually, record once at the boundary where errors are truly handled or reported, to avoid outputting the same failure repeatedly as it propagates through each layer.
If a public enum exposes third-party parser errors directly as variant fields, callers may start matching that specific type, and upgrading dependencies will affect the API. You can provide stable category accessors as needed, while keeping implementation details in private representations; the cost is that callers have less information they can directly depend on. non_exhaustive only affects external exhaustive matching; it does not automatically stabilize field types, error codes, or recovery semantics.
External APIs can return stable code, short message, and correlation identifiers, while internal logs retain more detailed causes; do not treat Debug or complete source chains as long-term compatible response formats. Tests can assert variants, key fields, source types, and necessary context, avoiding turning third-party full English error reports into contracts that must remain exactly the same forever.
First master the propagation semantics of Result and Option, then decide on error types and reporting methods, to avoid misunderstanding ? as an automatic recovery or logging mechanism.