Skip to content

Conversation

@Frando
Copy link
Member

@Frando Frando commented Oct 20, 2025

Description

Note: 90% of this PR are written by an AI (OpenAI codex) which I instructed in numerous prompts to convert iroh from n0-snafu/snafu to n0-error. I told it to keep context in error-reporting.agents.md, which I added to the PR (will be removed before merge) for easy alteration.*

Note: The following description is also AI generated.


Migrate snafu/n0-snafun0_error across iroh workspace

Rationale

  • Unify error handling on n0_error (meta, display, source chaining). Remove per-crate snafu/n0-snafu. Consistent ergonomics across libs, bins, and examples.

Highlights

  • Replace snafu/n0-snafu with n0_error in: iroh, iroh-relay, iroh-base, iroh-dns-server, benches, examples, docs.
  • Conversions: use .std_context(..) for std/3rd-party errors; .context(..) only for local StackError types; use .e() in examples/tests where needed for AnyError.
  • Macros: snafu::whatever!n0_error::whatever!; ensure_whatever! → explicit if !cond { whatever!(..) }.
  • add_meta/derive(Error): prefer #[error(from_sources, std_sources)] when feasible.

Gotchas

  • External errors (e.g., JoinError, WriteError, ReadToEndError, ClosedStream) don’t implement StackError in examples; use .e()? or .std_context(..).
  • Prefer bare ? for local StackError results; remove unnecessary .e() on internal Result<_>; re-add .e() only where compilation requires AnyError.

Review focus

  • Correct use of .context(..) vs .std_context(..).

Breaking Changes

Notes & open questions

Change checklist

  • Self-review.
  • Documentation updates following the style guide, if relevant.
  • Tests if relevant.
  • All breaking changes documented.
    • List all breaking changes in the above "Breaking Changes" section.
    • Open an issue or PR on any number0 repos that are affected by this breaking change. Give guidance on how the updates should be handled or do the actual updates themselves. The major ones are:

@Frando Frando mentioned this pull request Oct 20, 2025
11 tasks
@github-actions
Copy link

github-actions bot commented Oct 20, 2025

Documentation for this PR has been generated and is available at: https://n0-computer.github.io/iroh/pr/3561/docs/iroh/

Last updated: 2025-10-21T07:49:51Z

@n0bot n0bot bot added this to iroh Oct 20, 2025
@github-project-automation github-project-automation bot moved this to 🏗 In progress in iroh Oct 20, 2025
@github-actions
Copy link

github-actions bot commented Oct 20, 2025

Netsim report & logs for this PR have been generated and is available at: LOGS
This report will remain available for 3 days.

Last updated for commit: 298d409

Ok(len) => {
if len != PublicKey::LENGTH {
return Err(DecodeInvalidLengthSnafu.build());
return Err!(KeyParsingError::DecodeInvalidLength);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looking at this, I would much rather write

bail!(KeyParsingError::DecodeInvalidLength);

overloading Err I find problematic personally

Copy link
Member Author

@Frando Frando Oct 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I can add such a bail! macro. Without bail! or theErr! macro it would be
Err(e!(KeyParsingError::DecodeInvalidLength))

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that would be better to have than Err!

self.as_verifying_key()
.verify_strict(message, &signature.0)
.map_err(|_| SignatureSnafu.build())
.map_err(|_| e!(SignatureError))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this have to be a macro, or could this be a method on SignatureError?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, this is a struct so it can just be SignatureError::new() in this case. However, for enums I removed the constructors from the proc macro generated code (I had them before) so that jump-to-definition on variants works.

let expected = Self::KIND;
let Some(rest) = str.strip_prefix(expected) else {
return Err(KindSnafu { expected }.build());
return Err(ParseError::wrong_prefix(expected));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't this be called ParseError::kind(expected)?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nvm, found the impl below

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we have a macro so we can write

bail!(ParseError::Kind { expected });

without the indirection method?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this was just kept from existing code. It would either be bail or return Err!(ParseError::Kind { expected }) or return Err(e!(ParseError::Kind { expected })). Which one we want is to be bikeshedded.

} else {
let path = dirs_next::data_dir()
.context("operating environment provides no directory for application data")?;
.std_context("operating environment provides no directory for application data")?;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this works reasonably well, a bit confusing at first, but easy to use at the end of the day

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To me, this is the most annoying thing about this whole endevour, but unfortunately I don't think there's another solution. I made the std_context method the "longer" one (to type) to nudge towards using context wherever possible (ie. on our errors that implement StackError) because std_context will work on our errors too, but we'll lose the location info in the process. This is the much discussed issue that as long as we implement std::error::Error for our errors, we can't forbid calling the StdResultExt methods on them, even though we should always use the StackResultExt methods (which are only available on StackError results, not on std error results).

Ok(Err(err)) => {
warn!(?err, "task failed");
final_res = Err(err).context("task");
final_res = Err(err).std_context("task");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we have something like

final_res = whatever_std!(err, "task");

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can. Here it even can be the same macro for both StackError and std error, because we can use autoref specialization.

There is already a anyerr! macro that makes use of autoref specialization to not have to worry about whether the error is std or StackError. With this it would be Err(anyerr!(err).context("task")). I can expand the macro to support context as second arg, so it could be Err(anyerr!(err, "task")). Will do that.
Not sure if we need a variant that returns Err(err), I think it's fine to write that by hand (like anyhow::anyhow!, it also returns the error not a result).

url: dial_url.clone(),
}
.build()
e!(ConnectError::InvalidWebsocketUrl {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this macro fills in the location details?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. It expands to ConnectError::InvalidWebsocketUrl { url: dial_url.clone(), meta: Default::default() }

snafu::ensure!(size <= MAX_PACKET_SIZE, ExceedsMaxPacketSizeSnafu { size });
n0_error::ensure!(
size <= MAX_PACKET_SIZE,
n0_error::e!(SendError::ExceedsMaxPacketSize { size })
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we make the macro such that we don't need to call e! inside?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can but then it wouldn't be usable with other error types anymore as it would add a meta field and if that doesn't exist it would give a compile error. We could add another macro ensure_e! or such, but not sure if that is really better than the current one. Will change to import e though, then it's much shorter already.

})
.await??;
.await
.map_err(|err| e!(DialError::Timeout, err))??;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does this variant do?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expand to DialError::Timeout { source: err, meta: Meta::default() }
See docs: https://n0-computer.github.io/n0-error/pr/4/docs/n0_error/macro.e.html

let upgraded = hyper::upgrade::on(res).await.context(ProxyConnectSnafu)?;
let upgraded = hyper::upgrade::on(res)
.await
.map_err(|err| e!(DialError::ProxyConnect, err))?;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can't we do this with one of the context variants?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

context takes a string (or, impl Display) and adds this as context. We can't do without the e! macro if we want jump-to-usage on ProxyConnect to work and not type meta: Default::default() manually. So not sure what context should do here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, the filling in the struct wouldn't be possible :(

errors: Vec<LookupError>,
}

// no manual new; use e!(Type { errors })
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

random comment

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will remove

@Frando
Copy link
Member Author

Frando commented Oct 23, 2025

Some examples when tests are failing:

Error: Unable to connect to remote (at iroh/src/endpoint.rs:633:26)
Caused by:
    0: Unable to connect to remote (at iroh/src/endpoint.rs:735:28)
    1: endpoint stopping

Here we get the same message twice, because the outer error delegates to an inner error with #[transparent].

Failed to create bind sockets (at iroh/src/endpoint.rs:202:21)
Caused by:
    0: Failed to create bind sockets (at iroh/src/magicsock.rs:1389:31)
    1: failed to bind

Same: the outer error delegates to the next with transparent, so same message twice.

Another one:

Error: No addressing information available (at iroh/src/endpoint.rs:699:28)
Caused by:
    0: Discovery service failed (at iroh/src/endpoint.rs:1291:39)
    1: Discovery produced no results for b371e8d4c1 (at iroh/src/discovery.rs:645:21)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: 🏗 In progress

Development

Successfully merging this pull request may close these issues.

2 participants