skip to content

Checked-Exception Non-Rollback

A checked exception commits by default, which is the opposite of what most people expect; rollbackFor=Exception.class or rethrowing unchecked restores the intended behaviour. Interviewers ask precisely because the default surprises people.

part ofSpring Frameworkoverview, primer and where to startread it →
on this pageshow

questions

4

In a @Transactional Spring method, what happens to the transaction if a checked exception is thrown out of the method?

level: juniorimportance: must knowfreq 70%

answer

  1. checked = commit, unchecked = rollback
  2. rollbackOn: instanceof RuntimeException || Error
  3. EJB heritage default
  4. IOException/SQLException commit silently
  5. opt in with rollbackFor

basics

~10 s

By default Spring COMMITS the transaction. Spring only rolls back automatically on unchecked exceptions (RuntimeException and Error). A checked exception like IOException does not trigger a rollback unless you configure it.

solid answer

~40 s

Spring's default rollback rule rolls back only for unchecked exceptions — subclasses of RuntimeException and Error. A checked exception (anything extending Exception but not RuntimeException, e.g. IOException, SQLException, or a custom BusinessException extends Exception) propagates out but the transaction still COMMITS. This surprises developers who assume 'any exception = rollback'. To roll back on a checked exception you must opt in with @Transactional(rollbackFor = ...). This default comes from EJB conventions where checked exceptions modelled recoverable business outcomes and unchecked ones modelled system failures. The practical danger: a method throws a checked exception to signal failure, the caller sees the error, but the partial data was already persisted.

code

java · 18 lines
java
@Service
public class OrderService {

    // Custom CHECKED exception (extends Exception, not RuntimeException)
    public static class OrderException extends Exception {
        public OrderException(String m) { super(m); }
    }

    @Transactional
    public void placeOrder(Order o) throws OrderException {
        repository.save(o);              // INSERT happens
        if (!inventory.reserve(o)) {
            // Checked exception -> transaction still COMMITS!
            // The save above is NOT rolled back. Surprise.
            throw new OrderException("out of stock");
        }
    }
}

go deeper

for a junior

Must know the one-liner: checked commits, unchecked rolls back, by default.

for a middle

Should connect it to concrete types (IOException vs NPE) and know rollbackFor fixes it.

for a senior

Should explain the instanceof RuntimeException||Error mechanism and the data-integrity risk.

for a principal

Should note the EJB rationale, the Kotlin hierarchy nuance, and design implications for exception strategy.

## The core rule Spring's declarative transaction management (`@Transactional`) wraps your method in a proxy. When the method throws, the proxy consults a **rollback rule** to decide commit vs. rollback. The default rule, implemented in `org.springframework.transaction.interceptor.DefaultTransactionAttribute.rollbackOn(Throwable)`, returns `true` **only** when the throwable is an instance of `RuntimeException` or `Error`. So: - **Unchecked** exceptions — anything extending `RuntimeException` (e.g. `IllegalArgumentException`, `NullPointerException`, Spring's `DataAccessException`) or `Error` — trigger an automatic **rollback**. - **Checked** exceptions — anything extending `Exception` but NOT `RuntimeException` (e.g. `java.io.IOException`, `java.sql.SQLException`, or your own `class OrderException extends Exception`) — do **NOT** trigger rollback. The transaction **commits** even though an exception left the method. ## Why 'checked' vs 'unchecked' matters In Java, `RuntimeException` and its subclasses are *unchecked* (compiler doesn't force a `throws` clause). Everything else under `Exception` is *checked*. Spring's default maps directly onto this hierarchy: it literally does an `instanceof RuntimeException || instanceof Error` test. It is NOT looking at the `throws` keyword — it inspects the runtime type. ## Why the surprise is dangerous Consider a service that does two inserts and then, on a validation failure, throws a checked `BusinessException`. The developer expects both inserts undone. Instead Spring commits them. The caller receives the exception and logs 'failed', but the database now holds half-finished state. This is a classic source of data-integrity bugs. ## The rationale (EJB heritage) Spring inherited this from the EJB spec. The idea: checked exceptions are part of a method's contract and often represent **recoverable business conditions** the caller is expected to handle ("insufficient funds") — not necessarily a reason to discard work. Unchecked exceptions represent **unexpected system faults** — the safe default is to roll back. You can disagree with this default, which is exactly why Spring lets you override it. ## How to fix it (preview) - `@Transactional(rollbackFor = Exception.class)` — roll back on ANY exception, checked included. - `@Transactional(rollbackFor = MyCheckedException.class)` — roll back on a specific checked type. - Or throw/wrap in an unchecked exception instead of a checked one. - Or programmatically: `TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()`. ## Kotlin note Kotlin has no *checked* exceptions at the language level, but the JVM type hierarchy still exists. Spring still tests `instanceof RuntimeException`. So a Kotlin exception that extends `Exception` (not `RuntimeException`) will STILL not roll back by default — the rule is about the class hierarchy, not the `throws` keyword.

  • Which exception types DOES Spring roll back on by default?
    Only unchecked ones: any subclass of RuntimeException, plus Error. Everything else (checked exceptions extending Exception directly) commits by default.
  • If your method throws a NullPointerException, does the transaction roll back?
    Yes. NullPointerException extends RuntimeException, so it is unchecked and triggers the default rollback.

saying these in an interview costs you the question

  • Claiming any thrown exception always rolls back the transaction
  • Thinking Spring looks at the 'throws' clause rather than the runtime type
  • Believing checked exceptions roll back and unchecked ones commit (it's the reverse)

context

open as a page

How do you make a Spring transaction roll back when a checked exception is thrown?

level: middleimportance: must knowfreq 65%

basics

~10 s

Set rollbackFor on the annotation: @Transactional(rollbackFor = Exception.class) rolls back on any exception, or name a specific type like rollbackFor = MyCheckedException.class. Alternatively, throw an unchecked exception instead.

open as a page

Walk through the internal mechanism that decides commit vs. rollback in Spring, and why a checked exception commits by default.

level: seniorimportance: should knowfreq 40%

basics

~10 s

The transaction interceptor catches the thrown exception and calls rollbackOn(). The default (DefaultTransactionAttribute) returns true only for RuntimeException and Error, so checked exceptions fall through to commit. rollbackFor adds extra RollbackRuleAttribute entries.

open as a page

Kotlin has no checked exceptions, and your codebase is Kotlin. Does the 'checked exception commits by default' pitfall still apply, and how should you set a team-wide policy?

level: principalimportance: should knowfreq 25%

basics

~20 s

Yes it can still apply. Spring tests the runtime type, not the throws keyword. A Kotlin exception extending java.lang.Exception (not RuntimeException) still commits by default. In practice most Kotlin exceptions extend RuntimeException, so they roll back — but relying on that is fragile; set an explicit policy.

open as a page