Java Exceptions: Try, Catch, and Finally
Follow the path a program takes when an operation fails. Learn when to catch, when to propagate, and how to preserve both useful diagnostics and the last valid state instead of turning invalid input into fake success.
Before you start: review errors and debugging, method calls, and encapsulation. Each example is a complete runnable Java 21 Main.java program requiring no input.
What you will be able to do
- Trace normal and exceptional control flow through try and catch.
- Distinguish throwing an exception object from declaring a checked failure.
- Explain checked exceptions, unchecked exceptions, and compiler obligations.
- Preserve an underlying cause when translating an input error.
- Use finally for completion work without overriding returns or failures.
- Validate an update before assignment and handle only expected input failures.
An exception changes the path through your program
A numeric field can contain valid text such as 45 or invalid text such as oops. The calculation needs an int, while the input is a String. Integer.parseInt either returns a number or throws NumberFormatException; it does not return a partially valid number and continue the next statement.
In the first example, execution enters try and prints Before parse. The parse fails, so the remaining Accepted statement in that try block is skipped. A matching catch handles the exception, then execution continues after the try/catch statement because the handler finishes normally.
Catching a failure does not resume at the statement after the failed parse inside try. It selects another control-flow path. Keep the protected block small enough to understand which operation may fail and what successful work has already happened before the handler runs.
Main.java · See which statements an exception skips
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Before try
Before parse
Rejected: enter a whole number.
After catch
Trace the first failure in order
- The first two println calls run normally.
- Integer.parseInt throws before the assignment can complete.
- The Accepted line is skipped.
- The compatible NumberFormatException handler prints rejection.
- The handler finishes normally, so execution reaches After catch.
Change oops to 45 and predict the new path. The catch should not run on successful parsing.
Use throw to report an operation that cannot satisfy its contract
throw takes an exception object and causes abrupt completion at that point. Our requirePositive method rejects zero before returning a result. The caller catches IllegalArgumentException and can explain the invalid request rather than pretending that zero was accepted.
Prefer a meaningful exception type and a message that explains the violated rule. Do not use exceptions to replace every ordinary branch: checking whether a value meets a simple expected condition can still be normal if/else logic. The contract determines whether rejection is an exceptional failure for this operation.
Throwing an exception does not automatically undo previous assignments, external calls, or database changes. Validate before mutation when possible, and use the right transactional mechanisms for real persistent workflows. This lesson's model is in-memory only and performs no external updates.
Main.java · Throw an explicit contract failure
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Rejected: Value must be positive.
Let a caller handle a failure when it has useful context
parseField has no catch, so the parsing exception propagates out of that call. readMinutes also has no handler, so its call completes abruptly too. The catch in main matches the exception and reports the input problem at the caller boundary.
This is not the same as manually returning an exception object from each method. The runtime unwinds active calls looking for a compatible handler. If no handler handles the failure in that thread, the uncaught-exception mechanism is used; the exact effect on an application depends on its threads and framework.
Do not add a catch to every layer just to print the same stack trace repeatedly. Handle, propagate, or translate according to the layer's responsibility. A parser may describe invalid text, while a presentation boundary decides what a user sees; unrelated programming defects should not be silently swallowed there.
Main.java · Handle the failure at a caller boundary
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Caller handled invalid numeric text.
Checked describes a compiler obligation, not when a failure occurs
NumberFormatException and IllegalArgumentException are RuntimeException subclasses, so they are unchecked. The compiler does not require a catch or throws declaration for them, even though they can fail during execution. Unchecked does not mean harmless, impossible, or forbidden to catch.
InvalidMinutesException extends Exception directly in our example, so it is checked. A caller of a method declaring that failure must handle it or declare an appropriate checked exception itself. Both checked and unchecked exceptions are thrown at runtime; checked does not mean that the compiler has already executed the method.
Error subclasses are also unchecked but conventionally represent serious conditions rather than ordinary input validation. Catching Throwable to force apparent success is not a safe general policy. Choosing a custom checked exception here is a teaching API decision, not a rule that every invalid form field must use one.
Main.java · Fulfill a checked-exception obligation
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
45
Rejected: Minutes must be between 1 and 180.
throw performs the action; throws describes a method's checked contract
MinutesInput.parse declares throws InvalidMinutesException after its parameter list. That declaration tells callers which checked failure they must account for. It does not create an exception, automatically reject an argument, or guarantee the method will fail on every call.
Inside the method, throw new InvalidMinutesException actually reports a failure. A method can also propagate a checked failure from another method without creating a new one. StudySession.updateFromText declares the same checked type because it lets the parser's failure reach its own caller.
Adding throws to main can satisfy a compiler obligation while still leaving invalid input unhandled for the user. Conversely, declaring throws for an unchecked type is optional and does not make it checked. Choose whether to handle or propagate based on what the caller can usefully do, not just on what makes the diagnostic disappear.
finally runs as the try/catch completes, not as a success message
The finally example prints Attempt finished for a successful parse and for a handled numeric failure. The line means the attempt ended; it does not mean the input was valid. A finally block is attached to try and runs after the selected try or catch path as that statement completes.
A normally completing finally also runs during propagation of an uncaught exception, and before a pending return leaves the method. It does not by itself handle the exception or change a failed operation into success. A try can have finally without catch when completion work is required but handling belongs to a caller.
Do not promise that finally executes after forced process termination, a JVM crash, or a path that never exits its try. For resources that implement AutoCloseable, prefer the try-with-resources mechanism introduced in the next lesson rather than building fragile manual closing rules here.
Main.java · Trace finally on both normal and handled paths
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Parsed: 12
Attempt finished
Rejected numeric text
Attempt finished
Keep finally from replacing the result or the original failure
In choose, the return expression selects 7, then finally prints its line before the caller receives 7. Because finally completes normally, it preserves that pending return. The output order makes clear why completion work can happen even after the return statement has been reached.
A return, throw, or other abrupt completion inside finally can override a pending return or failure from try. Returning 0 in finally would replace 7; throwing a new exception there could hide the original one. Avoid control-flow exits from finally instead of using them as clever fallback logic.
Even innocent-looking completion code can fail, so keep it purposeful and do not use it to print success or overwrite validated state. Detailed cleanup failure and suppressed-exception behavior belongs with the next lesson's resource management. Here the finally blocks only print synthetic attempt markers.
Main.java · A normally completing finally preserves a pending return
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Preparing return
Finally before caller receives the result
Result: 7
Why is Result printed last?
- choose reaches return 7 in its try block.
- The finally block runs before control returns to the caller.
- finally finishes normally, preserving the pending value 7.
- The caller receives 7 and prints Result.
Do not add return or throw to finally as a shortcut. It can replace the outcome already in progress rather than merely run after it.
Translate expected input failures while preserving diagnostic evidence
MinutesInput.parse checks required input, then catches only NumberFormatException around Integer.parseInt. It wraps that underlying failure as InvalidMinutesException with a friendly domain message and the original cause. The caller can display the friendly message while a developer can inspect the cause chain.
A numeric-format failure can mean nonnumeric text or a value outside the int range; the message deliberately asks for a whole number of minutes instead of echoing raw input. Parsed integers outside 1 through 180 are rejected separately by the domain-range rule. Blank and range failures have no nested numeric cause because no numeric conversion failed.
Catching a superclass also matches compatible subclasses; for example NumberFormatException is an IllegalArgumentException. Broad catches can therefore handle more than you intended. Keep catches narrow in both type and protected scope, avoid echoing sensitive payloads, and reserve multiple catch clauses and multi-catch syntax for the next lesson.
Main.java · Translate an error without losing its cause
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Enter a whole number of minutes.
Cause: NumberFormatException
Separate three kinds of rejected input
- Missing or blank text: no numeric conversion was attempted, so there is no numeric cause.
- Malformed or out-of-int-range text: NumberFormatException is preserved as the cause.
- A parsed integer outside 1..180: conversion succeeded, but the domain rule rejects the value.
The caller receives the same checked domain type with a specific message. That does not mean every rejection has the same technical origin.
A rejected update must not damage the last valid value
StudySession begins at 25 minutes. updateFromText parses and validates into a local variable first, then assigns the field only after success. A successful 60-minute update is preserved when the following negative input is rejected.
The parser does not print and does not change the session. The session does not decide how to display an error. main handles the expected checked failure and reads the getter afterward, making the boundary between interpretation, mutation, and presentation visible.
Do not set the field to zero before parsing and hope a catch restores it. That introduces an unnecessary invalid intermediate state and makes every failure path harder to audit. Validation-before-assignment is simple here because the update is local and single-threaded; it is not a claim of transaction or concurrent-update safety.
Main.java · Rejected input leaves the last valid state intact
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Accepted: 60
Rejected: Minutes must be between 1 and 180.
Still: 60
Test successful paths, rejection paths, causes, and completion order
A parser test should cover accepted boundary values 1 and 180, padded valid input, blank input, malformed text, out-of-int-range text, and parsed integers outside the allowed domain. Verify the exception type and documented message instead of assuming every failed parse has the same cause.
For the session, establish a valid value, submit each rejected input, and check that the getter remains unchanged. Call a valid update again afterward to prove the object is still usable. A caught error is not automatically recovery unless the program's state and next action remain meaningful.
For control flow, predict which statements run before and after throw and finally. Keep a test for a failure propagating through a try/finally without a catch, and for a checked caller that fails compilation until it catches or declares the failure. The language rules and the model's business contract both need evidence.
Repair exercise: invalid input became a successful zero
The parser catches invalid numeric input and returns zero, so the caller prints a fake successful result. Keep the caller's specific handler reachable by rethrowing the original exception rather than inventing a numeric fallback. This focused exercise demonstrates propagation, not the full 1..180 parser contract.
Main.java · Find the swallowed parsing failure
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
The starter compiles and currently prints a misleading result:
Result: 0
Reveal the explained repair
throw error propagates the existing NumberFormatException and preserves its original diagnostic context. The caller now reports rejection instead of accepting zero. In this simple helper the redundant catch could also be removed entirely; the explicit rethrow makes the control-flow change visible for the exercise.
Main.java · Let the caller handle rejection
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Rejected: enter a whole number.
Independent lab: validate a minutes field without losing valid state
Build a checked minutes parser and a study-session update that preserves its last valid value after rejected input. Handle expected input failures at the presentation boundary and print an attempt-completion line from finally. This is a local in-memory exercise, not saved course progress.
- Define InvalidMinutesException extends Exception with message-only and message-plus-cause constructors. Keep all helper types package-private in the same Main.java file for the website runner.
- Implement MinutesInput.parse(String raw) throws InvalidMinutesException. Reject null/blank with Minutes are required. Strip accepted text, parse with Integer.parseInt, and wrap only NumberFormatException with Enter a whole number of minutes. while retaining its cause.
- Accept parsed integers 1..180 inclusive. Reject other parsed integers with Minutes must be between 1 and 180. This parser inherits Integer.parseInt syntax, including a leading plus sign; it is not a custom ASCII-digits-only grammar.
- Implement StudySession with private minutes initially 25, a minutes() getter, and updateFromText(String) declaring the checked failure. Parse into a local before assignment. Parser and model methods must not print or replace failed input with a default.
- In main process the fixed array 45, oops, 0, 181, and padded 60. Put try/catch/finally inside the loop so each expected input failure is handled separately. On success print Accepted: <minutes>; on failure print Rejected: <message> | kept: <minutes>; always finish the attempt with Attempt finished.
- Print the final value, and test boundary inputs, exact messages, optional causes, unchanged state after rejection, and successful reuse after failures. Do not catch unrelated RuntimeException, Error, or Throwable as expected form-input errors.
The two valid inputs update the session; the three rejected inputs preserve the previous value. The completed program should produce:
Accepted: 45
Attempt finished
Rejected: Enter a whole number of minutes. | kept: 45
Attempt finished
Rejected: Minutes must be between 1 and 180. | kept: 45
Attempt finished
Rejected: Minutes must be between 1 and 180. | kept: 45
Attempt finished
Accepted: 60
Attempt finished
Final minutes: 60
Main.java · Build the checked input boundary
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Stage 1 hints: define one meaningful failure contract
Extend Exception directly and pass messages and optional causes to super. Declare the parser's checked failure. Handle only NumberFormatException around parseInt, then apply the numeric range rule outside that catch.
Stage 2 hints: validate before assigning
StudySession stores 25 initially. Parse the requested update into a local variable; only assign to the field after the call returns successfully. Let the checked failure propagate without printing from the model.
Stage 3 hints: handle each attempt independently
Put try/catch/finally inside the loop. Catch only InvalidMinutesException for the expected input boundary, read the unchanged value on rejection, and let finally print a neutral completion line. Neither return nor assign a fallback value from finally.
Reveal the complete explained solution
Main.java · Complete checked minutes-input lab
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Each input has its own protected attempt, so one rejected value does not skip the rest of the array. The checked exception is handled where a friendly message can be shown. finally describes completion only, and the model assigns only after parsing succeeds. Invalid text retains its numeric cause without exposing that technical detail as a fake valid result.
Test changes and boundaries
- Accept 1 and 180, ordinary valid values, padded values, and +45. Reject null, empty, spaces, tab/newline-only text, malformed text, and integers outside both int and domain ranges.
- Verify blank and range errors have no cause, while malformed or out-of-int-range text retains NumberFormatException as the cause. Assert documented messages rather than printing raw input.
- Set a session to a valid value, reject several invalid updates, and confirm the field remains unchanged after each. Submit another valid value afterward to prove the object remains usable.
- Predict completion order on success, handled failure, pending return, and propagated failure through finally. Do not add return or throw to finally to manufacture successful control flow.
- Remove a checked handler in a copy and inspect the compiler's catch-or-declare obligation. Confirm an unrelated programming failure is not converted into the expected input error by an oversized catch.
Common mistakes and how to diagnose them
Assuming catch resumes inside the failed try
The remaining statements in that try path are skipped. Normal execution can continue after the selected handler finishes, not by rerunning or resuming the failed expression.
Using throws as if it threw an object
throws declares a method contract and a checked-exception obligation for callers. throw performs the actual abrupt transfer with an exception instance.
Returning a fake value from an input-error catch
Zero can look like a valid calculation result. Handle rejection explicitly or propagate the failure; do not silently replace invalid input with success.
Returning or throwing from finally
Abrupt completion in finally can replace the original result or exception. Keep completion work from hiding the operation's actual outcome.
Catching all failures as user mistakes
A broad catch can disguise a programming defect or serious Error as invalid form input. Protect only the operation and failure types the boundary can meaningfully interpret.
Assigning state before validation succeeds
Throwing does not roll back previous assignments. Parse and validate first, then commit the small local update so rejection preserves the last valid value.
Frequently asked questions
Are checked exceptions thrown during compilation?
No. They are runtime failures too. Checked means the compiler requires calling code to handle or declare the checked possibility; it does not execute the method or prove that a failure will occur.
What is the difference between throw and throws?
throw transfers control with an exception object. throws appears on a method declaration to describe checked failures it may propagate. A declaration alone neither constructs nor throws an exception.
Does finally always run?
It runs as the try/catch completes under ordinary execution, including propagation and pending returns. Forced process termination, a JVM crash, or a path that never completes its try can prevent it from executing.
Why preserve the original cause?
The domain message explains the failed operation to its caller, while the nested cause preserves the underlying numeric diagnosis for debugging. Omitting it can erase useful evidence.
Must every invalid field use a custom checked exception?
No. This lesson chooses one to make the compiler obligation explicit. Real APIs may choose a documented unchecked exception or an explicit validation result depending on their design.
Why does the catch go inside the input loop?
Each input is an independent attempt under this policy. A catch around the whole loop would leave that loop on the first failure and skip later values rather than continue with the next attempt.
Handle meaningfully, propagate deliberately, and preserve state
Exceptions skip the remaining normal path and seek a compatible handler. Checked types create a catch-or-declare obligation; throw reports an object and throws declares a contract. A normally completing finally preserves the pending result or failure, while a meaningful cause keeps diagnostic evidence available.
Explain why returning zero can hide an input error, why finally must not print Accepted, and why validation happens before assignment. Next: multiple exceptions and try-with-resources, where catch selection and cleanup failure need more deliberate handling.
Can you follow the failure and preserve the contract?
Answer six questions and read the explanations. Quiz answers are temporary and do not save course progress.
References and related learning
The minutes-input examples, explanations, and exercises are original SovranCode material. These official Java 21 references document exception checking, execution flow, and numeric parsing.
- Java 21 specification: exception types and checking
- Java 21 specification: try, catch, and finally execution
- Java 21 Integer API: parseInt behavior
Review debugging · Review encapsulation · Java course roadmap.
Last updated: