How to approach a huge number of compiler errors in a systematic way?

Here is an extract from a Rust-related discussion. I guess you all experienced this situation at some point:

I had 6 compiler errors, fixed 9 and now I have only 12.

It's indeed frustrating when you have a certain number of compiler errors, you fix a fraction of them and suddenly the compiler emits a much larger number of error messages. This might reduce motivation, and makes life harder if you need to check at what point you are in fixing the errors of some major dependency upgrade. The example above is very small, but I have personally seen situations in which there are 100s of error messages that have to be fixed.

Fortunately, there are some techniques that can be used to improve the situation:

  • Group and prioritize the error messages with something like

    cargo check | grep error | sort | uniq -c | sort -n
  • Comment the implementation of functions that don't compile and leave an unimplemented!(), make sure that the project compiles, then (one function at a time) revert the original function implementation and make it compile.

  • Understand the order of compiler checks and distinguish syntax errors from type-checker errors and from borrow-checker errors. Typically, errors follow an order: fixing a syntax or type error might unleash new type- or borrow-checker errors, but fixing a borrow-checker error should not uncover new syntax or type errors.

With this thread I would like to hear your experience and possibly suggestions. Are you aware of other tricks that make it easier to estimate how long fixing N compiler errors is going to take? Is there any tool that makes it easier to use one of the mentioned techniques? For example, some magic crate-level experimental macro that temporarily replaces each function implementation with unimplemented!(), or a tool that groups and orders error messages placing syntax errors before type-checker errors, and those in turn before borrow-checker errors?

1 Like

I've often had hundreds of errors while refactoring some projects, mostly because I heavily leverage macros. What I do is, i work with the phases of rustc:
First I fix up any syntactic issues.
Then type level issues.
And only after all of those are gone do I even bother focusing on borrowck / lifetime issues.

It also helps being prepared mentally for how rustc deals with errors. Otherwise it can be really discouraging to think that you've finally fixed all errors, only to be met by a new wall of errors from the next phase.


i don't have any tools for you, but one thing to pay attention to is that the kind of error you encounter will change when new errors appear. This is because, for example the compiler is not able to type check when there are still syntax errors.

Total number of errors is not really a meaningful metric of your progress. Sometimes multiple error messages are caused by a mistake in a single place, and sometimes one error message may require you to rethink a large part of your code. Also keep in mind that Rust compiler works step by step: for example, you won't get any type errors until you fix all syntax errors, and you won't get any borrow checker errors until you fix all type and name resolution errors. So you should just accept that the errors shown to you are likely not all errors.

I generally try to fix a few first errors and then recompile. I don't even look at the total number. Once you fix an error, it's likely that the next errors messages will not make sense anymore, so working through the whole list is not productive. I would not advice sorting errors in any way. Usually the first error in a list is the most relevant. Consider this example:

use std::fs::File;
use std::io::Reaad;

fn main() {
    let mut f = File::open("1").unwrap(); Vec::new());

The first error ("unresolved import std::io::Reaad") is the real error you should fix, and the second error ("no method named read found...") is caused by the first error.

When you finally get to the borrow checker stage, you may find out that your code structure doesn't work at all (e.g. because you tried to alias mutable references and didn't realize it) and you need to refactor everything again. There isn't really a way to prevent that. Also note that unimplemented! and other panics can hide borrow checker errors in the following code because NLL borrow checker understands that it is unreachable.

Another thing to aim for is to split you work into multiple smaller steps so that you can do one step and then fix all errors. That should help keep the number of errors lower.


On Unix

cargo watch -c -s "RUSTFLAGS=-Awarnings cargo check --tests --quiet --color=always 2>&1 | head -n 40" 
  • cargo watch updates the output of the console each time a file in the folder is saved;

    • To install it, simply run cargo install cargo-watch.
  • RUSTFLAGS=-Awarnings is an env var that disables1 the warnings, which may pollute the screen in between errors;

  • --tests to include the tests in the code being checked;

  • --quiet hides the Downloading, Checking, etc. progress bars;

  • --color=always ensures the output is colorized despite it being piped;

  • 2>&1 merges the stderr into the stdout, since pipes by default work on the latter;

    • (some shells may let you use |& as a shortcut for 2>&1 |, but not all do).
  • | head -n 40 limits the ouput to the first 40 lines of error, you can adjust this number to your liking.

    • This is the key thing: the first errors are usually the ones causing the other ones, so those should be fixed first.

Optionally, you can complete that command with -x check -x test -x doc:

cargo watch -c -s "..." -x check -x test -x doc

so that if the first check has no errors, then it proceeds to check with warnings and then test the code and then build the documentation (use -x run in the case of a binary crate).

1 This causes the dependent crates to be checked again, which can be annoying when also using an IDE with on-save checks that of course ignore your env var. In that case, having a .cargo/config file with:

rustflags = ["-A", "warnings"]

can be more convenient than an env var, at the cost of requiring some script to toggle the state.


I usually put something like:

#![cfg_attr(debug_assertions, allow(bad_style))]
#![cfg_attr(debug_assertions, allow(unused))]
#![cfg_attr(debug_assertions, allow(dead_code))]

because when my code is a work-in-progress mess rustc barfs a torrent of style nitpicks that drown real errors in the code. I wish the compiler was smarter about this: if there's a fatal error somewhere, don't display unrelated style lints.

  • cfg_attr(debug_assertions, …), because Rust has no correct way to check for debug compilation mode, and that's the least incorrect one.

  • and I silence warnings only in the debug mode, because I still want to be reminded to fix minor issues before releasing the code.


But unused and dead_code can indicate more than minor issues. These lints helped me catch a bug multiple times. For example, when I introduce a new function but forget to call it. If some item isn't going to be used for a while, I usually put an allow on the item. But even a module-level allow sometimes leads to missed bugs because of unused items, and crate-level allow seems more counter-productive.

Silencing bad_style is strange too. If you conform to the recommended style, you usually write new code already in that style, so these warnings don't come up. If you didn't conform to the recommended style, you would disable the lint unconditionally. Note that bad_style can catch a nasty bug when you match on a variant name with a typo in it (although unused would trigger on it too most of the time).

You would turn the lints off afterwards, so you would catch them then.

When doing large scale refactoring (e.g. changing the API for some fundamental type) I'll usually try to start at the root of the dependency tree and work my way outwards.

Depending on how large your project is you might have errors that cross multiple crate boundaries (e.g. you've got a workspace with 5 sub-crates and break something in the foo_core crate), and it helps to cd into just one crate, make sure that compiles again, then commit the changes before moving onto the next crate.

In one of the projects at work we decided to switch from using doubles everywhere to a datatype that's strongly typed (think uom). As a CAD/CAM package with a couple hundred thousand lines of code, you can imagine the massive number of compile errors that induced. The way I solved it was to work slowly and methodically in a side branch, breaking one field at a time and resolving all the resulting compile errors, then committing my work before moving to the next item.

Haha, nope. Sometimes fixing a particular compilation error is just a case of renaming a variable or passing an extra parameter to a function, other times you may need to redesign a module to resolve the problem.

I haven't heard of anything that'll magically resolve compile errors. If you find something, please let me know.

I think the best solution is to make sure it doesn't happen in the first place. Design things so the inter-component coupling is minimal, inserting seams (interfaces, callbacks, etc.) so you have frequent "borders" where breaking changes aren't able to cross. Write code keeping in mind that at some point business requirements will change and your architecture will need to be flexible enough to evolve with them.

I know they're sometimes helpful, but they're sometimes totally counter-productive. If I have typo in Car::neww(), then rustc discovers there's no instance of it, and then proceeds to discover that everything else it used became unused and prints "Wheel unused, Doors unused, Trunk unused" x100, and I don't see that one error about the critical typo that caused all of this.

In Sublime I have errors displayed inline, which often is great. Rustc's code-fix suggestions are useful, so I can type something roughly-correct, click [ Accept Suggestion ] button a few times and it'll work :slight_smile: But style nitpicks can make the whole file red and I don't see what am I doing.


Well, it depends on the editor I think - in VS Code with RLS we'll get the whole file yellow, with one line clearly red :slight_smile:

1 Like

Along with the excellent suggestion to use cargo watch -x check, I would also advise 'learn your editor'. For example, F8 in VS Code will take you to the next error (or warning, unfortunately) so it is very quick to move through them. (I specifically gave up on Intellij IDEA because I could not find an equivalent way of doing this). Also, learn how to search and replace with regexes. That can deal with a lot of simple refactorings.

1 Like


It's F2 by default, and it will jump only on errors if there are any, skipping warnings. I haven't tested it with Rust, but with Java it is this way.

I could swear that I had read about an option that causes rustc to stop immediately after encountering the first error (similar to gcc's -fmax-errors=n, for example), but when I checked a few days back I couldn't find it. Was I hallucinating?

Thanks a lot for all the suggestions!

Are these phases documented somewhere, in a way understandable to Rust beginners? I learned them by experience, but there must be a better way for new users.

I completely agree! It's not a good metric as your example shows, and I'm wondering what other useful information the Rust compiler could provide. For example, knowing how many files/modules passed the syntax/type/borrow checker would be much better. The compiler should already know some of this, to decide e.g. that the borrow checker should be run on a particular file/module only after the syntax and type checkers found no errors.

The phases rustc takes are implementation-defined, so I'm not sure there'll be much formal documentation available. I guess you could read through the rustc dev guide to see how the compiler has been implemented, although that's not exactly targeted at new users.

You generally pick it up pretty quickly as a byproduct of writing code that has errors. For example, if I have a syntax error and a type error, the compiler won't show me the type error until after the syntax error is fixed.

It also helps to have an understanding of how compilers are typically implemented and what information is needed at each stage. Normally you'll start off with a bunch of text, lex it into tokens, parse that into an AST, expand macros, convert the resulting AST into something more compiler-friendly (HIR), then apply type checking + inference, run the borrow checker, then start generating the code. I've probably messed up the order and skipped steps, but that's the general gist.

1 Like

In addition to the fact that this is not and never will/should be a "stable API" of sorts that we'd want to document this thoroughly, the most important parts are already fairly obvious and unchangeable. For example, rustc can't produce type errors for a function with syntax errors in it. If you know what "type" and "syntax" mean, that's clearly going to be true of just about every language. Likewise for type errors preventing any robust borrow checking. However, there might be cases where rustc hypothetically could guess what syntax you meant to write and do some speculative type checking anyway (there's already a lot of speculative parsing going on to deliver better syntax errors); I doubt this will ever happen, but we wouldn't want to rule it out by committing to "error phases".

But AFAIK, "syntax"->"type"->"borrow" already is the exhaustive list of clearly ordered phases that are likely to matter for a typical user trying to interpret their build errors. A lot of the other interesting stuff in Rust just isn't so clearly ordered. For example, there's no telling when the compiler will chose to run a const fn that needs compile-time evaluation (nor should there be; we need to do whatever's fastest for compile times). And name resolution is very interesting due to macros, but IIUC not in a way that creates meaningful well-defined error phases.

In C++ this is a more reasonable question because there are a lot of highly technical and non obvious phases and terms like "unevaluated context" and "overload set resolution" and "candidate selection" and "SFINAE" and "integer promotion" that all are part of the formal standard with stability guarantees that hardcore generic code actually needs to fully understand. But in Rust that's just not the case, and hopefully it never will be.

1 Like

This doesn't even touch on how rustc's architecture is slowly evolving from distinct phases to an on-demand "query system" .

For example, the query system might decide to schedule one function to go through the borrow checker while an unrelated impl block is still being converted to HIR. When things are evaluated on-demand it gets hard to reason about things in terms of "before" and "after".

1 Like

My experience is that after spending a little time with the Rust compiler these "phases" become pretty clear.

Rust has the best error messages of any compiler I have used, it's a pleasure to have a chat with rustc.

I must admit it's a bit of shock the first time you clean up that last error and BOOM out pours a dozen more!

1 Like

Inspired by some of the ideas above, I've written a little Perl-script to run cargo clippy and pull out the errors to show first. This works well for Emacs at least. It saves re-running when you want to see the warnings. It has been working fine so far although of course I may have missed some case. Save it as ~/bin/cargo-clippy-errors-first on UNIX, and cargo will run it when you do cargo clippy-errors-first.

#!/usr/bin/perl -w

die unless open IN, "cargo clippy 2>&1 |";

my @lines = ();

while (<IN>) {
    push @lines, $_;
    if (@lines >= 2 && $lines[@lines-1] =~ / --> / && $lines[@lines-2] =~ /^error/) {
        my $line2 = pop @lines;
        my $line1 = pop @lines;
        print "$line1$line2";
        while (<IN>) {
            print $_;
            last if ($_ =~ /^\s*$/);

print @lines;

die "FAILED\n" unless close IN;
1 Like