SE-0489: Improve EncodingError and DecodingError's printed descriptions

Hi everyone,

The review of SE-0489: Improve EncodingError and DecodingError's printed descriptions begins now and runs through July 22, 2025.

Reviews are an important part of the Swift evolution process. All review feedback should be either on this forum thread or, if you would like to keep your feedback private, directly to the review manager via the forum messaging feature. When contacting the review manager directly, please keep the proposal link at the top of the message.

What goes into a review?

The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the direction of Swift. When writing your review, here are some questions you might want to answer in your review:

  • What is your evaluation of the proposal?
  • Is the problem being addressed significant enough to warrant a change to Swift?
  • Does this proposal fit well with the feel and direction of Swift?
  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

More information about the Swift evolution process is available here.

Thank you,

Xiaodi Wu
Review Manager

24 Likes

Very big +1, this is great!
I'd prefer to shorten the CodingKey(stringValue: "population", intValue: nil) ("population") to just "population" if possible, but that's my only nit-pick!

3 Likes

This would be possible with a further/downstream change to Foundation's encoders/decoders. I was investigating this originally, but it's nearly impossible for someone outside Apple to build and test changes to Foundation and the stdlib at the same time, and I was encourages to split out the stdlib changes into their own proposal. I'm definitely interested in further refining how the encoders/decoders construct these errors once we know exactly what the changes to the stdlib are going to look like. (This proposal gives a general suggestion for the debug description format, but leaves the specifics open to refinement as part of the PR.)

2 Likes

This is good. I could have used this last week.

The feature 'print context of surrounding lines' would also be good.

2 Likes

Seems like an easy +1. Good work!

1 Like

+1

1 Like

Would love to see this, +1.

1 Like

Very glad to see this! +1

1 Like

The proposal conforms two previously existing stdlib types to a previously existing stdlib protocol. This is technically an ABI breaking change: on ABI-stable platforms, we may have preexisting Swift binaries that implement a retroactive CustomDebugStringConvertible conformance, or binaries that assume that the existing error types do not conform to the protocol.

Would you be able to expand on this any more? What kind of "bad things" could happen after we ship a change like this? How would that "bad thing" manifest to a software engineer building a product? How would that "bad thing" manifest to a customer using a product?

We do not expect this to be an issue in practice, since checking an arbitrary error for conformance to CustomDebugStringConvertible at run-time seems unlikely. In the event that it now conforms where it didn't before, it will presumably use the new implementation instead of whatever fallback was being provided previously.

Here might be an example to think through:

If that value was a EncodingError or DecodingError — which I'm not completely sure is even possible through that code… but maybe it could happen — what could the effect be from this change shipping in stdlib?

I see a few places in stblib that try casting to CustomDebugStringConvertible… but since that check already is in stdlib I assume there would be no unexpected behavior there after this change ships.

I paraphrased that section from this proposal's spiritual predecessor, SE-0445. @lorentey, as the author of that proposal, I'd love to hear if you have anything to add, and whether there was any negative feedback after Swift 6.1 shipped. I'll venture my own justification for why this change is ok:

In places where someone is doing run-time checks such as the one you linked to, it will be in a context where they do not already know what type they are dealing with (or else they would already know whether they could call debugDescription).

In the linked code from Swift Testing, for example, passing an EncodingError or DecodingError currently falls through to the if valueTypeInfo.isSwiftEnumeration { branch, which looks like this:

Here is a snippet of test code to exercise that code path:

enum FooCodingKey: String, CodingKey { case bar }
let error = DecodingError.dataCorrupted(
  DecodingError.Context(
    codingPath: [FooCodingKey.bar],
    debugDescription: "stuff happened here"
  )
)
print(String(describingForTest: error))

It prints:

.dataCorrupted(Swift.DecodingError.Context(codingPath: [FooCodingKey(stringValue: "bar", intValue: nil)], debugDescription: "stuff happened here", underlyingError: nil))

After the conformance ships, it would hit the earlier as? any CustomDebugStringConvertible branch, as you point out. The output of the above example would look like this (reminder that the specific details of the output format are subject to change):

Data was corrupted.
Debug description: stuff happened here
Path: bar

If I'm reading this output for while trying to understand a test failure, this seems like a win to me. And, since, the error types in question are public, it is my hope that anyone who cared about the precise value of a particular error would be switching on it directly, rather than relying on its default String.init(describing:) behavior.

Maybe someone is recording a text-based snapshot test to confirm that a particular error is returned? That could result in a spurious test failure when they update the stdlib. But I would lump automated tests into the "this is a feature for debugging" category: it may be annoying to have to re-capture snapshots after updating your toolchain, but it's not going to cause a user-facing crash or bug, and the end result is clearer error messages.

It's true that there should be no surprises within the stdlib, but if any of those dynamic casts are used to produce values that are returned to client code, it's technically possible that it could invalidate some assumption that someone made somewhere. I'd still make the case that any such assumptions are overwhelmingly likely to be in the context of testing or debugging code, and are therefore acceptable.

Part of this review process is to suss out any such cases that might be concerning, so please let me know what you think about the above, and keep the feedback coming!

4 Likes

+1

1 Like

I'm excited about having this as I've grown to loathe these error messages.

I'm curious what would happen to clients who get this version of the standard library but they already extended EncodingError or DecodingError to CustomDebugStringConvertible . This leads to a new duplicate conformance warning, which in codebases that treat warnings as errors, becomes a breaking change for them.

Were it me, I'd probably just delete my conformance and take the standard library, but my questions are:

  1. Generally, do we care that proposals can cause compilation failures when changing type via protocol extension due to some number of project's configurations?
  2. Are we okay with the work-around for these failures being the deletion of client code?

I think this is kind of an interesting corner case to work through that seems apparent in this case. I would be surprised if I was the first person to ever think of this, and I'm raising this point more so there's continued documentation or a reference to previous conversations about this.

Thanks for your contribution Zev! While this is a technical feasibility of adopting your proposal as is, I'm not sure I would hold it up just because a number of clients could have to delete their conformances.

1 Like

This is great! Making sure that things print in a succinct but informative way is crucial to a great development experience -- every time we show an unreadable dump, we make a bad impression.

I paraphrased that section from this proposal's spiritual predecessor, SE-0445. @lorentey, as the author of that proposal, I'd love to hear if you have anything to add, and whether there was any negative feedback after Swift 6.1 shipped.

Your example for code that changes behavior based on what foo is CustomDebugStringConvertible returns is precisely the sort of thing that could cause binary compatibility issues -- these conformance tests are dynamic, so existing binaries can start seeing the new conformance where they did not see it before, potentially causing them to break.

Another potential problem is that there may be existing binaries that have shipped with their own retroactive CustomDebugStringConvertible conformances on the same types. When running those binaries, the Swift runtime will find two competing conformances, and it has to dynamically choose a single one to apply for the entire running process. No matter which one it chooses, code that assumes the other conformance implementation might break. (The runtime tends to prefer the custom conformance in favor of the standard one. This avoids breaking application code in the short term, but as parts of the system get used to the new standard conformance, they will inevitably start depending on its behavior -- so system libraries can eventually start breaking in weird ways when running these applications.)

The likelihood and severity of these issues varies wildly, depending on the specific protocols. Luckily, neither of these problems is particularly likely to get triggered by Custom[Debug]StringConvertible. As far as I am aware, adding the conformance to String.Index has not resulted in any binary compatibility issues whatsoever, and I expect coding errors are even less likely to hit them.

This makes me confident enough to say that we should feel free to add CustomDebugStringConvertible conformances to any type that needs one -- we can still acknowledge the potential bincompat issues without overly worrying about them. (In the unlikely case they do trigger, we may just need to delay or roll back an accepted proposal while we investigate mitigations.)

6 Likes

Maybe another POV I had was to what extent — if any — do we know these potential side effects (like undefined behavior) are localized to code that operates on EncodingError and DecodingError? Would there be any reason to assume that shipping this change results in side-effects on unrelated types… like Array or Dictionary? Does shipping this change "break" our ABI in weird and unusual ways… or can we confidently say that we know in exactly what ways ABI was broken?

If the side effects are localized and contained only for code operating on EncodingError and DecodingError and we know these side effects do not include crashes… then it sounds reasonable to me to go ahead and ship it.

This might be better feedback for a diff review… but do we have any option to ship this in a way that toolchains with assertions enabled would crash at runtime if two conformances are detected (instead of dynamically choosing one and continuing from there)?

+1 It is a nice improvement

Some thoughts:

  1. For decoding source with line breaks, e.g. configuration files, including a line number would be a nice addition and is generated by other parsers.

  2. Explicitly state that the path is sufficient for tools to highlight the source error.

  3. In future directions, provide an option to explicitly point to the exact location of the issue. This would be more important for "packed" JSON.

1 Like

Thanks for the feedback! All of these suggestions sound useful, but I think they are out of scope for this proposal.

For line numbers and other context information, we would have to include more information in DecodingError.Context and friends.

I believe we could add fields without ABI problems (can someone please confirm?), but the issue is that the change would have to be made concurrently in two libraries: We'd have to change the stdlib, where the error types live, in order to add the fields; and we'd have to change Foundation, where the encoders/decoders live, in order to populate those fields when constructing the error. Orchestrating such cross-project changes is very challenging for those of us outside Apple.

Perhaps there could be a path forward where we add things iteratively, maybe adding optional properties to Context first, and then populating them in a subsequent PR to Foundation. However, I think that would leave us with some unfortunate baggage, where fields are optional that wouldn't need to be if we could land everything together.

One more issue here could be that the Foundation encoders/decoders deal with Data, not files, so they might be able to know the line number, but they won't know the file name.

Using the path to locate and highlight the source of the error is something that can already be done using the information in DecodingError.Context.codingPath and friends. I demonstrated one such use in the library I built to explore some options here. But in terms of the formatted path in the output of debugDescription, this proposal makes no guarantees about the specific format. It only suggests possible formatting changes to make error printouts more readable and approachable. In general, we should not rely on the output of debugDescription for parsing/highlighting/etc, and should instead inspect the underlying values directly.

I covered something like this in the “Print context of surrounding lines in source data” portion of the proposal. Are you suggesting that we point more specifically, like how compiler errors draw ASCII art arrows to point to a line/column? I'm all for it, but since this is just a sketch of future directions, I'm not sure we need to go into that much detail here.

If you haven't seen The future of serialization & deserialization APIs, a thread where folks are discussing future iterations of serialization in Swift, I encourage you to check it out. I think there's a real opportunity to shape the error output to be as nice possible from the beginning!

1 Like

+1 for this improvement, the current error message is hard to read.

Overall this is a nice improvement, the existing errors are very difficult to parse so that's a great change.

In general though the CustomDebugStringConvertible are pretty weird if they are "nicely human readable" especially when they start to include multi-line output. The CustomDebugStringConvertible output is used also when a value is contained in an array... so an array of errors collected from a few things would suddenly become unexpectedly multi-line output. This behavior of Swift can be really unexpected but that's what we have so I'm concerned about any multi-line strings in any of the Custom...StringConvertible conformances, especially if the description also drops the error type as the proposed one does here:

Key 'population' not found in keyed decoding container.
Debug description: No value associated with key CodingKeys(stringValue: "population", intValue: nil) ("population").
Path: [0]/home/country

From looking at this output in a log dashboard in a backend system, I wouldn't know that this was a DecodingError unless I also logged the error type explicitly. So this change implies that we should also explicitly log error types and cannot trust the descriptions anymore to include them. This is probably already a good practice, but something worth calling out as it may cause some disruption.

The multi-line descriptions are my only real concern really:

Wouldn't it be safer make the message: one line, where a failure we're reporting looks like Key 'population' not found in keyed decoding container. No value associated with key CodingKeys(stringValue: "population", intValue: nil) ("population"), at path: [0]/home/country, and if there are multiple errors or notes, separate them with ;?

// edit: last example for clarity

3 Likes

This is a great point, and including the error type (and specific case?) is probably within the scope of the proposal. We can also follow precedent if there are examples of debug description formats that seem like a good fit here.

Would you mind providing an example of the output that you’re concerned about, and why multi-line in particular would be a problem? Is there a convention of debug descriptions being on one line? I used multi-line to help with readability, but if there are other tradeoffs to consider, we can definitely massage it in the PR.

I have to echo @ktoso concern here. CustomDebugStringConvertible is a weird protocol and it is not only used when debugging but also commonly called in other places to implement description from CustomStringConvertible. The server ecosystem is making use of description heavily for logging and any multi-line string will most likely break logging backends.

So unless I am missing something, I am a -1 on this proposal without it addressing these concerns around logging.