When things go wrong
Tell syntax, runtime and logic errors apart, choose between exceptions and error values, and handle bad input deliberately.
What you will learn
- Tell syntax errors, runtime errors and logic errors apart
- Explain the difference between exceptions and error values
- Handle an exception with try and catch, and know when not to
- Validate input at the boundary and fail fast with a clear message
On this page
Every program meets trouble sooner or later: a file that is missing, a user who types “forty-two” where a number belongs, a division by zero, a plain mistake in your own logic. Programs that handle trouble well are not ones that never fail. They fail in a way that is clear, safe and recoverable. This lesson covers the kinds of error, the two main ways languages report them, and how to respond.
Three kinds of error
Syntax errors break the grammar of the language: a missing bracket, a misspelt keyword. The program is rejected before it runs (a compile error, or an error when an interpreter first reads the file). They are the easiest to fix because the message points at the line.
Runtime errors happen while the program runs, even though the code is grammatical: dividing by zero, opening a file that does not exist, reading past the end of a list, converting “abc” to a number. The program stops (or you handle the error).
Logic errors are the dangerous ones. The program runs without complaint and gives the wrong answer, because you told it to do the wrong thing. No message appears. Only testing, and thinking, will find them.
Syntax errors
Take the line that prints hi but forgets the closing bracket. Every language refuses it, in its own words and at its own moment:
| Language | Incomplete line | What happens |
|---|---|---|
| Python | print("hi" |
SyntaxError when the file is first read, before any line runs |
| JavaScript | console.log("hi" |
SyntaxError when the file is first parsed, before any line runs |
| Java | System.out.println("hi" |
the compiler reports an error and produces no program |
| C | printf("hi" |
the compiler reports an error and produces no program |
In every case nothing runs at all, which is why these are the cheapest errors to fix.
Runtime errors
Valid code can still fail while it runs. Dividing a whole number by zero is the classic case, and the four languages disagree about it:
Python
try: print(10 / 0)except ZeroDivisionError as err: print("runtime error caught:", err)runtime error caught: division by zeroPython raises an exception, which the except block catches.
JavaScript
console.log(10 / 0);InfinityJavaScript does not treat this as an error at all: floating-point division by zero is defined to give Infinity. Bad results can pass silently here, so check the divisor yourself.
Java
public class RuntimeError { public static void main(String[] args) { int divisor = 0; try { System.out.println(10 / divisor); } catch (ArithmeticException err) { System.out.println("runtime error caught: " + err.getMessage()); } }}runtime error caught: / by zeroJava throws an ArithmeticException for whole-number division by zero, which the catch block handles.
C
#include <stdio.h>
int main(void) { int divisor = 0; if (divisor == 0) { printf("runtime error avoided: divisor is zero\n"); } else { printf("%d\n", 10 / divisor); } return 0;}runtime error avoided: divisor is zeroC has no exceptions. Dividing by zero is undefined behaviour and usually crashes the program, so the only defence is to test the divisor first.
Logic errors
A logic error produces no message. This function should average two numbers, but the precedence mistake a + b / 2 divides only b:
Python
def average(a, b): return a + b / 2 # bug: should be (a + b) / 2
print(average(10, 20), "but the average of 10 and 20 is 15")JavaScript
function average(a, b) { return a + b / 2; // bug: should be (a + b) / 2}
console.log(average(10, 20), "but the average of 10 and 20 is 15");Java
public class LogicError { static double average(double a, double b) { return a + b / 2; // bug: should be (a + b) / 2 }
public static void main(String[] args) { System.out.println(average(10, 20) + " but the average of 10 and 20 is 15"); }}C
#include <stdio.h>
double average(double a, double b) { return a + b / 2; /* bug: should be (a + b) / 2 */}
int main(void) { printf("%g but the average of 10 and 20 is 15\n", average(10, 20)); return 0;}The output is 20.0 but the average of 10 and 20 is 15 in Python and Java, and 20 but ... in JavaScript and C. No exception will ever warn you about it: the code is valid, runs to the end and returns the wrong number.
Two ways to report a problem
When a function cannot do what it was asked, it has to tell its caller somehow. There are two main styles.
Error values. The function returns a special value meaning “that did not work”: None, -1, NaN, a null pointer, or a result object that records success or failure. C usually works this way. The caller must remember to check, and nothing forces it to.
Exceptions. The function raises (throws) an exception, an object describing the problem. Normal execution stops at once and the runtime unwinds the call stack, frame by frame, looking for code that has said it can catch that kind of exception. If none does, the program ends with an error message and a stack trace. Python, Java and JavaScript all have exceptions.
Exceptions have a big advantage: an error can’t be silently ignored, because an uncaught one stops the program. They also let the code that detects the problem be far away from the code that knows what to do about it. Error values are simple and visible in the code, but forgetting to check one leads to subtle bugs.
Here is an error-value function: find the position of an item in a list, and report “not found” with a special value. Python returns None; the other three have no such value for a number, so they return -1, a position that cannot exist.
Python
def find_index(items, wanted): for i, item in enumerate(items): if item == wanted: return i return None
print(find_index(["a", "b", "c"], "b"))print(find_index(["a", "b", "c"], "z"))1NoneJavaScript
function findIndex(items, wanted) { for (let i = 0; i < items.length; i++) { if (items[i] === wanted) { return i; } } return -1;}
console.log(findIndex(["a", "b", "c"], "b"));console.log(findIndex(["a", "b", "c"], "z"));1-1Java
public class FindIndex { static int findIndex(String[] items, String wanted) { for (int i = 0; i < items.length; i++) { if (items[i].equals(wanted)) { return i; } } return -1; }
public static void main(String[] args) { String[] items = {"a", "b", "c"}; System.out.println(findIndex(items, "b")); System.out.println(findIndex(items, "z")); }}1-1C
#include <stdio.h>#include <string.h>
int find_index(const char *items[], int count, const char *wanted) { for (int i = 0; i < count; i++) { if (strcmp(items[i], wanted) == 0) { return i; } } return -1;}
int main(void) { const char *items[] = {"a", "b", "c"}; printf("%d\n", find_index(items, 3, "b")); printf("%d\n", find_index(items, 3, "z")); return 0;}1-1Nothing forces the caller to look at that -1. Use it as a position and you get a bug, or in C, a read from outside the array.
Now the other style. The same task in four languages: convert text to a whole number and cope with bad input. Notice that each language uses a different mechanism.
Python
for text in ["42", "forty-two"]: try: n = int(text) except ValueError: print(f"{text!r} is not a number") else: print(f"{text!r} -> {n}")JavaScript
for (const text of ["42", "forty-two"]) { const n = Number(text); // no exception: bad input gives the error value NaN if (Number.isNaN(n)) { console.log(`'${text}' is not a number`); } else { console.log(`'${text}' -> ${n}`); }}Java
public class ErrParse { public static void main(String[] args) { String[] inputs = {"42", "forty-two"}; for (String text : inputs) { try { int n = Integer.parseInt(text); System.out.println("'" + text + "' -> " + n); } catch (NumberFormatException e) { System.out.println("'" + text + "' is not a number"); } } }}C
#include <stdio.h>#include <stdlib.h>
int main(void) { const char *inputs[] = {"42", "forty-two"}; for (int i = 0; i < 2; i++) { char *end; long n = strtol(inputs[i], &end, 10); /* end points at the first unread character */ if (end == inputs[i] || *end != '\0') { printf("'%s' is not a number\n", inputs[i]); } else { printf("'%s' -> %ld\n", inputs[i], n); } } return 0;}All four print:
'42' -> 42'forty-two' is not a number- Python and Java use exceptions:
int(text)andInteger.parseInt(text)raiseValueErrorandNumberFormatException, and thetry/except(Java:try/catch) block responds. - JavaScript’s
Number("forty-two")does not throw. It returns the error valueNaN(“not a number”), so the code tests for it. (Other JavaScript operations, such asJSON.parseon bad text, do throw.) - C’s
strtolreturns a value and reports problems through the pointerend: if no digits were read, or something is left over, the input was not a clean number.
In the try form, code in the try block runs normally. If it raises an exception matching the except clause, the rest of that block is skipped and the handler runs. Python’s optional else block runs only when nothing was raised.
Handling errors deliberately
Having a way to catch errors does not mean you should catch everything. Some guidelines:
- Catch specific errors, not “anything at all”. A handler that swallows every exception hides bugs along with the problem you expected.
- Only catch what you can do something about: ask the user to retry, use a default, skip a bad record, or add context and let it continue upward. If you can’t recover, let it propagate.
- Never write an empty handler. An error that disappears without a trace is the hardest kind to debug. At minimum, log it.
- Clean up resources (files, connections) whatever happens. Languages offer a
finallyblock or awith/try-with-resources form for this. - Don’t use exceptions for ordinary control flow when a simple
iftest would do.
Validate input and fail fast
Treat anything coming from outside your program (user typing, files, network, other programs) as untrusted. Validate it at the boundary, where it enters, and reject bad data immediately with a clear message. Fail fast means detecting a problem as close as possible to where it begins, rather than letting wrong data travel through ten functions and explode far from its source.
This function states what an age may be and refuses anything else, with a message that includes the offending value. Python, JavaScript and Java raise an exception; C has none, so it returns an error code that the caller must check.
Python
def set_age(age): if age < 0 or age > 150: raise ValueError(f"age must be between 0 and 150, got {age}") return age
try: set_age(-3)except ValueError as err: print("rejected:", err)JavaScript
function setAge(age) { if (age < 0 || age > 150) { throw new RangeError(`age must be between 0 and 150, got ${age}`); } return age;}
try { setAge(-3);} catch (err) { console.log("rejected:", err.message);}Java
public class SetAge { static int setAge(int age) { if (age < 0 || age > 150) { throw new IllegalArgumentException("age must be between 0 and 150, got " + age); } return age; }
public static void main(String[] args) { try { setAge(-3); } catch (IllegalArgumentException err) { System.out.println("rejected: " + err.getMessage()); } }}C
#include <stdio.h>
/* Returns 0 on success, -1 if the age is out of range. */int set_age(int age, int *result) { if (age < 0 || age > 150) { return -1; } *result = age; return 0;}
int main(void) { int stored; if (set_age(-3, &stored) != 0) { printf("rejected: age must be between 0 and 150, got %d\n", -3); } return 0;}All four print:
rejected: age must be between 0 and 150, got -3Good error messages say what happened, what was expected and, if possible, what to do about it. “age must be between 0 and 150, got -3” is far more useful than “invalid input”.
Common pitfalls
- Catching everything (
except:orcatch (Exception e)) and carrying on as if nothing happened. - Empty handlers that silently discard the error.
- Forgetting to check an error value, such as using the result of
strtolor aNoneor-1without testing it. - Validating late, so that bad data has already been stored or used before anyone checks.
- Vague messages that say something failed but not what, where or why.
- Treating a logic error as a runtime one. Wrapping buggy arithmetic in
trydoes not make the answer correct.
Say whether each is a syntax error, runtime error or logic error: (a) print("hi" with no closing bracket; (b) total = a - b where you meant to add; (c) opening data.txt when no such file exists; (d) limit = 10 / n where n is 0 (answer for a language where this raises an error). Expected: one label for each of the four.
Show solution
(a) Syntax error: the grammar is broken and the program won’t start. (b) Logic error: valid code, wrong answer, no message. (c) Runtime error: the code is fine, but the environment doesn’t match, so an exception is raised. (d) Runtime error: division by zero is detected only when that line runs.
Write calculate(text) for input like "6 / 3" (number, operator, number). It must return the result for good input and a clear error message (not a crash) for each of these bad inputs: a divisor of zero, non-numeric operands, an unknown operator, and the wrong number of parts. Try it on these six inputs:
| Input | Expected |
|---|---|
6 / 3 |
2 (shown as 2.0 in Python and Java) |
4 / 0 |
error: division by zero |
abc + 1 |
error: operands must be numbers |
5 * |
error: expected 'number operator number' |
2 ^ 3 |
error: unknown operator '^' |
1.5 * 4 |
6 (shown as 6.0 in Python and Java) |
Show solution
The function validates the shape first, converts the numbers with the language’s own parsing tool, and checks for the one runtime failure we can predict (zero) with an ordinary if. Each failure returns a specific message.
Python
def calculate(text): parts = text.split() if len(parts) != 3: return "error: expected 'number operator number'" left, op, right = parts try: a = float(left) b = float(right) except ValueError: return "error: operands must be numbers" if op == "+": return a + b if op == "-": return a - b if op == "*": return a * b if op == "/": if b == 0: return "error: division by zero" return a / b return f"error: unknown operator {op!r}"
for line in ["6 / 3", "4 / 0", "abc + 1", "5 *", "2 ^ 3", "1.5 * 4"]: print(f"{line!r} -> {calculate(line)}")JavaScript
function calculate(text) { const parts = text.trim().split(/\s+/); if (parts.length !== 3) { return "error: expected 'number operator number'"; } const [left, op, right] = parts; const a = Number(left); const b = Number(right); if (Number.isNaN(a) || Number.isNaN(b)) { return "error: operands must be numbers"; } if (op === "+") return a + b; if (op === "-") return a - b; if (op === "*") return a * b; if (op === "/") { if (b === 0) { return "error: division by zero"; } return a / b; } return `error: unknown operator '${op}'`;}
for (const line of ["6 / 3", "4 / 0", "abc + 1", "5 *", "2 ^ 3", "1.5 * 4"]) { console.log(`'${line}' -> ${calculate(line)}`);}Java
public class SafeCalc { static String calculate(String text) { String[] parts = text.trim().split("\\s+"); if (parts.length != 3) { return "error: expected 'number operator number'"; } double a; double b; try { a = Double.parseDouble(parts[0]); b = Double.parseDouble(parts[2]); } catch (NumberFormatException e) { return "error: operands must be numbers"; } String op = parts[1]; switch (op) { case "+": return String.valueOf(a + b); case "-": return String.valueOf(a - b); case "*": return String.valueOf(a * b); case "/": if (b == 0) { return "error: division by zero"; } return String.valueOf(a / b); default: return "error: unknown operator '" + op + "'"; } }
public static void main(String[] args) { String[] lines = {"6 / 3", "4 / 0", "abc + 1", "5 *", "2 ^ 3", "1.5 * 4"}; for (String line : lines) { System.out.println("'" + line + "' -> " + calculate(line)); } }}C
#include <stdio.h>#include <stdlib.h>
/* Parse a whole token as a number; return 1 on success. */static int parse_number(const char *token, double *out) { char *end; *out = strtod(token, &end); return end != token && *end == '\0';}
static void calculate(const char *text, char *result, size_t size) { char left[32], right[32]; char op; if (sscanf(text, "%31s %c %31s", left, &op, right) != 3) { snprintf(result, size, "error: expected 'number operator number'"); return; } double a, b; if (!parse_number(left, &a) || !parse_number(right, &b)) { snprintf(result, size, "error: operands must be numbers"); return; } switch (op) { case '+': snprintf(result, size, "%g", a + b); break; case '-': snprintf(result, size, "%g", a - b); break; case '*': snprintf(result, size, "%g", a * b); break; case '/': if (b == 0) { snprintf(result, size, "error: division by zero"); } else { snprintf(result, size, "%g", a / b); } break; default: snprintf(result, size, "error: unknown operator '%c'", op); }}
int main(void) { const char *lines[] = {"6 / 3", "4 / 0", "abc + 1", "5 *", "2 ^ 3", "1.5 * 4"}; for (int i = 0; i < 6; i++) { char result[80]; calculate(lines[i], result, sizeof result); printf("'%s' -> %s\n", lines[i], result); } return 0;}C fills a buffer that the caller supplies, because returning text from a function needs memory management that comes later in the track.
Python and Java print:
'6 / 3' -> 2.0'4 / 0' -> error: division by zero'abc + 1' -> error: operands must be numbers'5 *' -> error: expected 'number operator number''2 ^ 3' -> error: unknown operator '^''1.5 * 4' -> 6.0JavaScript and C print the same lines, except that the first and last results are 2 and 6 (they do not add a .0).
Returning message strings keeps the example short; in a larger program you would more likely raise an exception (or return an error code) for bad input and handle it at the place that talks to the user.
A function find_user(id) looks up a user in a database. A missing user is a normal, expected outcome. A broken database connection is not. How would you report each? Expected: one sentence for each case, naming the style (error value or exception).
Show solution
For the missing user, an ordinary return value (such as None, null or an empty result) works well, since callers expect it and checking it is routine. For the broken connection, raise an exception (or, in C, return a distinct error code that cannot be mistaken for a user): it’s unexpected, the caller of find_user probably can’t fix it, and it must not be silently ignored.
Summary
- Syntax errors stop a program from starting, runtime errors stop it mid-run, and logic errors give wrong answers silently.
- Errors are reported either as special return values (which must be checked) or as exceptions (which propagate until caught).
- Catch specific exceptions you can handle, never leave handlers empty, and clean up resources.
- Validate input at the boundary and fail fast with a message that says what went wrong.
That completes the second module of the shared track. The next module covers data structures, beginning with Arrays and lists.