Warnings & Errors
A program that compiles is not the same thing as a program that is correct. Learning to read errors, warnings, linker failures, runtime crashes, and silent logical bugs — and knowing which build stage each one comes from — is what turns "it compiled" into "it works."
C gives you very little safety net, which means the compiler, the linker, and the running program itself are constantly leaving you messages about what might be wrong. Some of those messages stop the build outright, some let it continue while quietly warning you, and some never appear at all even though the program is producing the wrong answer. This page works through every category of problem you'll run into — compile-time errors, syntax and semantic mistakes, warnings and why they deserve attention, linker errors, runtime failures, and logical bugs that hide in plain sight — along with how to read a diagnostic message, why the first error in a wall of text is usually the one that matters, and the habits that keep a codebase warning-free instead of warning-blind.
Errors, Warnings, and the Bigger Picture
Before looking at any single example, it helps to separate the four broad categories of "something is wrong with this program" that C developers run into, because each one is diagnosed differently and at a different point in the program's life.
An error means a tool — usually the compiler — genuinely cannot make sense of, or cannot finish producing, the program, so it refuses to continue. A warning means the tool understood the code well enough to build it anyway, but noticed something that looks like it could be a mistake. Runtime errors and logical errors, by contrast, have nothing to do with whether the program compiled at all — they only show up once the program is actually running.
The missing semicolon after printf("Hello\n") is a genuine compile-time error. GCC reports something like error: expected ';' before 'return' — the compiler cannot correctly process this file, so no executable is produced at all.
Category | Meaning | Typically stops the build?
----------------|----------------------------------------------------|----------------------------
Error | The tool cannot correctly process the program | Yes
Warning | The tool found something suspicious or risky | No
Runtime error | A problem happens while the program is executing | N/A (no compilation issue)
Logical error | The program runs fine but produces the wrong result | No#include <stdio.h>
int main(void) {
printf("Hello\n")
return 0;
}Compile-Time Errors
A compile-time error happens while the compiler is translating your source code into machine-level instructions, before anything is ever run. If the compiler cannot make sense of a line, it cannot safely guess what you meant, so it stops and reports the problem instead of producing an executable.
Common causes include syntax errors, missing semicolons, invalid declarations, references to identifiers that were never declared, incompatible constructs, malformed function definitions, and expressions the compiler cannot parse or type-check.
This line is missing its terminating semicolon. The compiler cannot tell where the statement ends, so it reports a compile-time error rather than guessing.
Adding the semicolon back is enough to make this a syntactically valid statement.
int x = 10int x = 10;Syntax Errors
A syntax error is a specific kind of compile-time error: the source code does not follow C's grammar at all, independent of what it's trying to say. Mismatched parentheses, missing braces, and stray punctuation all fall into this bucket, and the compiler detects them directly while parsing your file.
The condition's closing parenthesis after 10 is missing, so if (x > 10 { is not valid C grammar at all — the parser cannot even determine where the condition ends and the block begins.
Restoring the closing parenthesis makes this a grammatically valid if statement.
if (x > 10 {
printf("big\n");
}if (x > 10) {
printf("big\n");
}Semantic Errors
A semantic error is the opposite kind of mistake: the code is grammatically valid C — the parser has no complaint — but it violates the language's usage rules once its meaning is considered. The compiler still has to reject or flag it, just for a different reason than a syntax error.
This line parses perfectly fine as a declaration with an initializer — there's nothing grammatically wrong with it. But assigning a string literal (a pointer to char) to a plain int is a type mismatch, which is a semantic problem. Depending on the compiler and the options in use, this is reported as a warning or an outright error.
int x = "hello";What a Warning Actually Means
A warning is the compiler's way of saying "I accepted this code and will build it, but something about it looks suspicious." Unlike an error, a warning does not stop the build — the compiler still produces an object file or executable.
x is declared and given a value but never used anywhere afterward. The program compiles and runs correctly, but a compiler such as GCC with -Wall will print warning: unused variable 'x' — the code still works, yet the warning is pointing at something that is very likely a mistake, like a variable you meant to use and forgot.
It's tempting to treat a warning as noise because the program "still works." In practice warnings routinely reveal typos, wrong assumptions about a type, unintended conversions, unused variables, uninitialized values, missing declarations, mismatched format strings, and portability problems — all things that can turn into real bugs later even though nothing is wrong today.
#include <stdio.h>
int main(void) {
int x = 5;
printf("Done\n");
return 0;
}Why Warnings Matter
Some of the problems a warning catches are genuinely dangerous precisely because they don't look dangerous. Reading a variable's value before it has ever been given one is a good example — the code compiles, the program usually runs, and it may even print a plausible-looking number, purely by accident.
x is never initialized before being read. A compiler will typically warn with something like warning: 'x' is used uninitialized. Whatever value gets printed is whatever bits happened to already be sitting in that memory location — it could be 0, it could be garbage, and it can even change between runs.
Ignoring this class of warning is how uninitialized-value bugs survive all the way from a quick local test into shipped software — the program "worked" every time the programmer happened to run it, purely by luck of what was in memory.
#include <stdio.h>
int main(void) {
int x;
printf("%d\n", x);
return 0;
}Common Compiler Warnings
A handful of warning categories come up constantly enough that it's worth being able to recognize each one by sight. Below are five of the most common, each with the pattern that triggers it.
Unused variable — count is assigned but never read, so it can't be affecting the program's behavior at all.
Unused parameter — value is accepted by the function but never referenced in its body.
Implicit conversion — assigning a double to an int truncates the fractional part without asking, which the compiler flags as a possible loss of data.
Signed/unsigned comparison — before comparing, C converts index to unsigned under the usual arithmetic conversions, so -1 becomes a very large unsigned number, and index < size can evaluate in a way that looks wrong at a glance.
Format mismatch — %f tells printf to read a double-sized argument, but value is an int, so the two disagree about how the argument should be interpreted.
int count = 10;
// count is never referenced again anywhere in this scopevoid process(int value) {
// value is never used inside this function
}double temperature = 36.7;
int value = temperature;
// value becomes 36 — the fractional .7 is silently discardedint index = -1;
unsigned int size = 10;
if (index < size) {
// this branch's behavior can be surprising
}int value = 7;
printf("%f\n", value); // wrong: %f expects a double, value is an int
printf("%d\n", value); // correctPromoting Warnings to Errors
Because warnings don't stop a build on their own, it's easy for a project to slowly accumulate dozens of them that nobody ever looks at again. Most compilers let you flip that behavior around and treat every warning as if it were a hard error instead.
-Werror turns any warning that would normally be printed into a build failure instead. This is a common setting in professional projects specifically so that new warnings can never quietly enter the codebase — a warning either gets fixed immediately, or the build simply won't produce a program.
gcc -Wall -Wextra -Werror program.c -o programImportant Warning Options
GCC and Clang expose a family of flags that control which categories of warnings get printed at all. A typical development command turns on several of them at once.
-Wall enables a large, commonly-useful set of warnings. -Wextra adds further checks that -Wall leaves out. -Wpedantic flags code that relies on compiler-specific extensions rather than strict standard C. Adding -Werror on top of these turns the whole set into hard failures for stricter builds.
Despite the name, -Wall does not mean "enable every warning the compiler is capable of." It enables a substantial, well-established set of checks, but there are additional warning categories that must be requested separately (as -Wextra and -Wpedantic already show). Treating -Wall as "the maximum warning level" is a common misunderstanding.
gcc -Wall -Wextra -Wpedantic program.c -o programLinker Errors
Not every build failure comes from the compiler. After every source file has been compiled into an object file, a separate tool called the linker combines those object files (and any libraries) into one final executable, matching up every function call with an actual definition of that function somewhere.
greet is declared, so the compiler is satisfied that calling it is legal and compiles main.c into an object file without complaint. But because greet is never actually defined anywhere, the linker fails afterward with something like undefined reference to `greet'.
That single example shows the real build pipeline a C program travels through: source code goes into the compiler, the compiler produces an object file, the linker combines object files and libraries into an executable. A compiler error and a linker error are failures at two completely different stages of that pipeline.
#include <stdio.h>
void greet(void); // declared, but never defined anywhere
int main(void) {
greet();
return 0;
}Compiler Error vs Linker Error
Because both eventually show up as "the build failed," it's worth contrasting the two directly by where in the pipeline each one happens.
A compiler error means the translation step itself failed — there is no object file to show for it. A linker error means translation succeeded completely and an object file exists, but the final assembly step could not find everything it needed to produce an executable.
Compiler error example: int x = ;
source ──▶ compiler ❌
(never reaches an object file at all)
Linker error example: calling greet(); with no definition
source ──▶ compiler ✔ ──▶ object file ──▶ linker ❌
(compiles cleanly, fails only when combining object files)Runtime Errors
A program can compile without a single warning and link into a complete executable, and still fail once it's actually running. These are runtime errors — problems that only exist while the program is executing, not while it's being built.
a / b compiles and links without any complaint — dividing two ints is completely ordinary code. But dividing by zero is invalid, and depending on the platform this is undefined behavior that can terminate the program abnormally when it actually runs.
Other common runtime errors include invalid memory access, dereferencing a null pointer, stack overflow from runaway recursion, a file operation that fails, a memory allocation that fails, and simply running out of some other resource the program needed.
#include <stdio.h>
int main(void) {
int a = 10;
int b = 0;
printf("%d\n", a / b);
return 0;
}Logical Errors
The hardest category to catch is the logical error: the program compiles with no errors, produces no warnings, and doesn't crash — it runs from start to finish and prints an answer. The answer is just wrong.
This program is syntactically fine and semantically fine — adding two ints is completely legal C, and nothing about it looks unusual to the compiler. The bug is purely in the programmer's head: a rectangle's area is length times width, not length plus width. No tool used here can catch this — only checking the actual output against what it should be can.
Logical errors are exactly why "it compiled with zero warnings" is not the same claim as "it's correct." Only testing — comparing actual output to expected output — catches this class of bug.
#include <stdio.h>
int main(void) {
int length = 5;
int width = 4;
int area = length + width; // should be length * width
printf("Area = %d\n", area);
return 0;
}The Four Major Categories, as a Mental Model
It helps to keep one simple tree in mind whenever a C program isn't behaving: almost every problem you'll hit sorts into one of four buckets.
Compile, link, and runtime errors all stop something outright — the build, or the program itself. Warnings and logical errors are the two categories that let everything keep running while still signaling that something may be, or definitely is, wrong.
C Program Problems
|
┌───────────────┬───────┴────────┬──────────────────┐
│ │ │ │
Compile errors Link errors Runtime errors (also, off to the side:)
(invalid C) (missing symbols) (invalid execution) Warnings Logical Errors
(suspicious code) (wrong behavior)
— don't stop — don't stop
a build a buildAnatomy of a Diagnostic
Every message a compiler prints — whether it's an error or a warning — is built from the same handful of pieces, and learning to pick them apart by eye makes every future diagnostic faster to read.
That single line tells you the file (main.c), the line number (12), the column number (9), the severity (warning, as opposed to error), and the message itself describing what was found. Modern compilers often add further context underneath — a caret pointing at the exact spot, or a suggested fix — but these five pieces are always present in some form.
main.c:12:9: warning: unused variable 'count'
└──┬──┘ ┬ ┬ └───┬───┘ └───────────┬───────────┘
file line col severity messageReading an Error Message Step by Step
Applying the same breakdown to a real compiler error makes the message far less intimidating than it looks as one dense line of text.
File: main.c. Line: 15. Column: 10. Severity: error. Description: expected ';' before 'return'. Once split apart like that, the message says exactly one specific thing — the compiler expected a semicolon somewhere before it reached the word return on line 15 — rather than reading as an opaque wall of text.
The important part of an error message is rarely just the very last line the compiler prints. Read the first relevant diagnostic in full — file, line, column, and description together — rather than skimming straight to whatever scrolled by last.
main.c:15:10: error: expected ';' before 'return'The First Error Is Often the Most Important
A single missing semicolon can make the compiler report five, ten, or even dozens of subsequent errors, because once the parser loses track of where one statement ends, everything that follows can look malformed too — even though only one thing was actually wrong.
The reliable strategy is: find the first meaningful diagnostic, fix only that one, recompile, and see what's left — rather than trying to independently understand and fix every single message in a long list of output, most of which may disappear on their own once the real, original mistake is corrected.
Warnings Are Not Always Bugs
A warning doesn't automatically mean the code is wrong — sometimes the code is doing exactly what was intended, and the warning is just the compiler being unable to tell the difference between "forgot to use this" and "deliberately don't need this." callback matches a signature that requires an int event parameter, but this particular implementation has no use for its value. Casting it to (void) tells both the compiler and any future reader "yes, I know this parameter is unused, and that's on purpose" instead of leaving it looking like an oversight.
The (void)event; pattern is easy to misread as some kind of special language feature or a way to "delete" the parameter — it isn't. It's just an expression statement that evaluates event and discards the result, which happens to be enough to make the compiler stop flagging it as unused. Reach for it only after deciding the non-use really is intentional, never as a reflexive way to make a warning disappear.
void callback(int event) {
(void)event; // deliberately unused - silences the warning
printf("Callback triggered\n");
}Deciding Whether to Suppress a Warning
Sometimes a particular warning really is unavoidable or genuinely intentional, and compilers provide ways to suppress specific warnings rather than fixing the code that triggers them. That capability is easy to misuse, though, so it should always follow a deliberate decision process rather than being a reflex.
Before suppressing anything, ask: why did this warning happen in the first place? Is it pointing at a real problem? Could the code simply be rewritten to avoid triggering it at all? Only once you're confident the answer is "yes, this really is intentional" should you suppress it — and even then, document why, right at the point of suppression, so the next reader isn't left guessing.
The tempting shortcut — disabling warnings globally for a whole file or project, rather than addressing or documenting one specific, known warning — doesn't make the underlying issues go away. It just makes every other warning invisible along with the one you meant to silence, which is how real bugs slip through unnoticed later.
Warnings in Professional Development
A mature C workflow doesn't treat "it compiled" as the finish line. It aims for all of the following together: no compiler errors, no unexpected warnings, passing static analysis checks, passing tests, and clean runtime diagnostics.
Safety-critical or otherwise high-reliability software (flight systems, medical devices, automotive control code) typically enforces this far more strictly than a hobby project would — often with much larger sets of warnings enabled, -Werror on by default, and mandatory static analysis before code can even be reviewed.
Compiler Warnings vs Static Analysis Tools
Compiler diagnostics are a byproduct of translation — the compiler is already reading every line to generate machine code, and warnings are things it happens to notice along the way. A static analyzer is a separate tool built specifically to study a program's possible behavior in much greater depth, without ever running it.
Static analysis tools routinely catch things ordinary compiler warnings don't: possible null-pointer dereferences several calls deep, resource leaks (a file or allocation that's never released on some path), suspicious control flow, dead code that can never execute, broader data-flow problems, violations of a project's coding standard, and even potential concurrency issues. Clang-Tidy and Cppcheck are widely used free examples, alongside various commercial analyzers used in industry.
Warnings vs Errors vs Undefined Behavior
These three terms get used loosely in conversation, but they mean genuinely different things and shouldn't be treated as synonyms.
A warning is a tool telling you "this may be a problem" while still proceeding. An error is a tool telling you "I cannot continue this build stage at all." Undefined behavior is a statement from the C standard itself: the language imposes no requirements whatsoever on what happens once your program does something the standard defines as undefined.
Dividing by zero here may or may not be diagnosed by a particular compiler — some will warn, some won't, and behavior can differ by platform. Regardless of whether any tool says anything at all, the standard itself imposes no requirement on what this program does when it runs. Undefined behavior is a property of the program's execution, not a category of warning message.
int x = 5;
int y = 0;
int result = x / y;Treat Warnings as Development Feedback
The most productive way to think about all of this is as a loop that repeats constantly during development, rather than a one-time gate you pass through right before shipping.
Diagnostics are meant to be part of that ongoing cycle — read every time, acted on every time — rather than something skimmed past on the way to seeing whether the program merely compiled.
write code → compile → read diagnostics → fix errors
→ investigate warnings → run tests → use static analysis
→ debug remaining problems → (back to writing code)Best Practices
A handful of habits consistently separate codebases that stay healthy from ones that quietly accumulate problems over time.
Enable useful warnings (-Wall -Wextra -Wpedantic) on every build, not just occasionally. Keep the build genuinely warning-free — zero errors and zero unexpected warnings, not "a few we've been meaning to look at." Use -Werror where appropriate, but deliberately, since upgrading a compiler version can introduce brand-new warning categories that suddenly fail a -Werror build. Always read the complete diagnostic — file, line, column, message, and any extra context — rather than reacting to just the word "error" or "warning" by itself. Fix the cause, not the symptom: one root mistake can generate ten diagnostics, and fixing the original problem is what actually clears the list. Avoid disabling warnings globally. Combine compiler diagnostics with static analysis, since each catches defect classes the other doesn't. And test even when compilation succeeds cleanly — a successful, warning-free compilation proves nothing at all about whether the program is correct.
A Complete Before-and-After Example
Putting several of these ideas together, here's a small program with two separate problems stacked on top of each other, followed by the corrected version.
Two distinct problems live in this short program at once: value is read before ever being given a value (uninitialized read), and %f is used for what should be an int (format mismatch — %f expects a double). Both are things a compiler with -Wall -Wextra will flag, and both would still leave the printed output meaningless even if the compiler stayed silent.
value is now initialized before it's used, and %d correctly matches its type. Both diagnostics disappear because the underlying problems were actually fixed, not merely silenced.
It's worth noticing that the original version could easily have printed something that looked plausible — an uninitialized int can happen to contain a small, ordinary-looking number. A program "looking fine" on one run is not evidence that it's correct.
#include <stdio.h>
int main(void) {
int value;
printf("Value = %f\n", value);
return 0;
}#include <stdio.h>
int main(void) {
int value = 10;
printf("Value = %d\n", value);
return 0;
}A Mental Model for a Failed Build
When a build fails and it isn't obvious why, it helps to walk the same fixed pipeline every time, checking one stage at a time rather than guessing.
Following this top to bottom narrows the search immediately: if there's no object file at all, the problem is in preprocessing or compiling; if there's an object file but no executable, it's the linker; if there's a complete executable but it misbehaves once running, the problem is a runtime error or, if it just gives the wrong answer with no crash, a logical error.
Build
│
▼
Preprocessor ──❌──▶ preprocessing error (bad #include, macro problem, ...)
│ ✔
▼
Compiler ──❌──▶ compile error (syntax/semantic problem, no object file)
│ ✔
▼
Object file
│
▼
Linker ──❌──▶ link error (undefined reference, missing library)
│ ✔
▼
Executable
│
▼
Run ──▶ possible runtime problem (crash, UB, wrong result)Quick Reference
As a last summary, here's where some of the most common problems in this page actually happen, mapped against the pipeline above.
What is the core difference between an error and a warning when the compiler encounters them?
An error means the tool cannot correctly process the program and normally stops the build entirely; a warning means the tool found something suspicious but still finishes the build.
How does a syntax error differ from a semantic error, even though both can be flagged by the compiler?
A syntax error means the code doesn't follow C's grammar at all, so it can't even be parsed. A semantic error means the code is grammatically valid and parses fine, but it violates a usage rule once its meaning is considered, such as assigning incompatible types.
Why can a linker error happen even when every source file compiles without any errors or warnings?
Compiling only checks that each source file translates into a valid object file; it doesn't require that every function actually be defined somewhere. The linker runs afterward and fails separately if it can't find a definition for something that was called, producing an 'undefined reference' error even though compilation succeeded.
Why are logical errors considered the hardest category of problem to catch?
A logical error produces no compiler error, no warning, and no runtime crash — the program runs to completion and prints an answer that simply happens to be wrong, so only comparing the actual output against the expected output can reveal it.
Why should you generally focus on the first error message in a long list of compiler output rather than trying to fix every message individually?
A single early mistake, like one missing semicolon, can throw off the parser badly enough to generate many further errors that are really just consequences of that one root cause. Fixing the first meaningful diagnostic and recompiling often makes most of the rest disappear on their own.
Write a C program that declares a variable, initializes it, but never uses it afterward, so that compiling it with -Wall reports an unused variable warning.
unusedValue is given a value but is never referenced again anywhere in the program. The compiler still produces a working executable, but -Wall reports warning: unused variable 'unusedValue' because a variable that's never read is very often a sign of a forgotten line of code or a leftover from editing.
Write a C program with a function parameter that is intentionally never used inside the function, and suppress the resulting warning using the (void) cast pattern.
onEvent must accept an eventCode parameter to match the shape callers expect, even though this particular implementation has no need for its value. (void)eventCode; evaluates the parameter and discards the result, which tells the compiler the non-use is deliberate rather than an oversight, silencing the unused-parameter warning without hiding any other warnings in the file.
Write a C program that reads and prints the value of an uninitialized int variable, to demonstrate why an uninitialized-value warning matters.
reading is declared but never given a value before printf reads it. The program still compiles and runs, but the printed number is whatever bits happened to already occupy that memory location — it is not a meaningful sensor value at all, which is exactly the kind of silent bug the 'used uninitialized' warning exists to catch.
Write a C program that assigns a double value to an int variable and prints both values, to show the data lost by an implicit conversion.
Assigning a double to an int is legal C, so the program compiles, but it silently truncates the value rather than rounding it — 36.7 becomes 36. A compiler with -Wall or -Wextra flags this as a possible loss of data specifically because the conversion happens without any explicit cast showing the programmer intended it.
Write a C program that compares a negative signed int against an unsigned int in an if condition, to demonstrate a signed/unsigned comparison warning.
Before the comparison runs, C's usual arithmetic conversions convert the signed index to unsigned, turning -1 into a very large unsigned value. That makes index < size false even though -1 is mathematically less than 10, which is exactly the surprising behavior the signed/unsigned comparison warning is meant to flag.
Write a C program that correctly prints an int value using printf, using a format specifier that actually matches the argument's type.
%d correctly matches the type of score, which is an int. Had this instead used %f (which tells printf to expect a double-sized argument), the mismatch between the format specifier and the actual argument type would be undefined behavior and would typically be flagged by the compiler as a format warning.
Write a C program that declares a function's prototype separately from its definition, and calls it from main, to show the pairing needed to avoid an undefined-reference linker error.
Declaring greet lets the compiler accept the call to greet() inside main and produce an object file successfully. Because greet is also actually defined later in the same file, the linker can find a matching definition for every call, and the build produces a complete executable. If the definition were removed, compilation would still succeed, but the linker would fail with 'undefined reference to `greet'.
Write a C program that computes the area of a rectangle using the correct formula, avoiding the common logical error of adding the dimensions instead of multiplying them.
A rectangle's area is length times width, so the multiplication here is what makes the result correct. Writing length + width instead would still compile with no warnings and run without crashing, producing 9 instead of 20 — a purely logical error that only comparing the output against the expected result would ever catch.
Write a C program that checks whether a divisor is zero before performing integer division, to avoid the undefined behavior that dividing by zero would cause.
Checking b == 0 before performing a / b avoids ever executing the division at all when the divisor is zero. Without that check, a / b with b equal to 0 is undefined behavior for integer division, which can crash the program or behave differently across systems rather than reporting a controlled, predictable error message.
Problem | Stage | Example / Note
-----------------------------|-----------------------|--------------------------------
Missing ; | Compilation | Syntax error
Invalid declaration | Compilation | Compiler error
Unused variable | Compilation | Warning
Suspicious conversion | Compilation | Warning (e.g. double -> int)
Missing function definition | Linking | Linker error (undefined reference)
Missing library | Linking | Linker error
Null dereference | Runtime | Invalid execution
Division by zero | Runtime / diagnostic | Undefined behavior
Buffer overflow | Runtime | Undefined behavior
Wrong formula | Program execution | Logical error