This guide covers Groovy exception handling with 12 tested examples. Learn try-catch-finally, custom exceptions, multi-catch, and error handling best practices on Groovy 5.x.
“Exception handling is not about preventing errors. It is about deciding what happens when errors inevitably occur – and making sure your program keeps its promises.”
Joshua Bloch, Effective Java
Last Updated: March 2026 | Tested on: Groovy 5.x, Java 17+ | Difficulty: Beginner to Intermediate | Reading Time: 18 minutes
Things go wrong. Files disappear, networks drop, users enter garbage data, and APIs return unexpected responses. If you have been writing Groovy code for any length of time, you have already encountered exceptions – probably a NullPointerException or a FileNotFoundException. The question is not whether your code will throw exceptions. The question is: what happens when it does?
Groovy exception handling builds on Java’s familiar try-catch-finally mechanism, but with some important differences. Groovy treats all exceptions as unchecked – you never have to declare throws on your methods. This makes code cleaner, but it also means you need to be more deliberate about where and how you catch errors.
In this tutorial, we will walk through 12 practical, tested examples covering everything from basic try-catch to custom exception classes, multi-catch blocks, exception handling inside closures, and best practices for production code. After working through them, you’ll have a solid understanding of how to write reliable Groovy code that handles failures gracefully.
If you are new to Groovy, start with our Groovy Hello World tutorial. If you want to understand how assertions relate to error handling, take a look at our Groovy Assert guide. And if closures are still a mystery, our Groovy Closures tutorial will get you up to speed.
Table of Contents
What Is Exception Handling in Groovy?
Exception handling is the mechanism by which your program detects, reports, and recovers from runtime errors. In Groovy (and Java), this is done through the try-catch-finally construct.
According to the official Groovy documentation on semantics, Groovy’s exception handling shares the same syntax as Java but with one key difference: Groovy does not enforce checked exceptions. In Java, if a method throws a checked exception like IOException, the calling code must either catch it or declare it in a throws clause. In Groovy, all exceptions are treated as unchecked – the compiler never forces you to handle them.
Here is the basic structure:
- try – wraps the code that might throw an exception
- catch – defines what happens when a specific exception occurs
- finally – runs cleanup code regardless of whether an exception occurred (optional)
The exception hierarchy in Groovy is identical to Java:
Throwable– the root of all errors and exceptionsError– serious JVM problems you should not catch (e.g.,OutOfMemoryError)Exception– problems your code can potentially recover fromRuntimeException– unchecked exceptions (e.g.,NullPointerException,IndexOutOfBoundsException)
Why Exception Handling Matters
Without proper exception handling, a single unexpected error can crash your entire application, corrupt data, or leave resources (files, database connections, network sockets) open and leaking. Good exception handling gives you:
- Graceful degradation – instead of crashing, your application can log the error, show a user-friendly message, and continue running
- Resource safety – finally blocks and try-with-resources ensure files and connections are properly closed
- Debugging information – well-structured exception handling preserves stack traces and adds context, making bugs easier to find
- Separation of concerns – error handling logic stays separate from business logic, keeping code clean and readable
Now let us see all of this in action with 12 practical examples.
12 Practical Examples
Each example below is self-contained – you can copy it into a Groovy script file and run it directly with groovy filename.groovy. All examples have been tested on Groovy 5.x with Java 17+.
Example 1: Basic try-catch
The simplest form of exception handling. Wrap risky code in a try block and catch the exception that might be thrown. In this example, we intentionally divide by zero to trigger an ArithmeticException.
Basic try-catch
// Basic try-catch: catching a division by zero
def divide(int a, int b) {
try {
def result = a / b
println "Result: $result"
} catch (ArithmeticException e) {
println "Error: Cannot divide by zero!"
println "Exception message: ${e.message}"
}
}
divide(10, 2)
divide(10, 0)
divide(42, 7)
Output
Result: 5 Error: Cannot divide by zero! Exception message: Division by zero Result: 6
Notice how the program continues running after catching the exception. Without the try-catch, the division by zero would crash the script entirely. The e.message property gives you a human-readable description of what went wrong.
Example 2: Multiple catch Blocks
When a block of code can throw different types of exceptions, you can handle each one differently with multiple catch blocks. The order matters – catch the most specific exception first, then the more general ones.
Multiple catch Blocks
// Multiple catch blocks: handling different exception types
def parseAndDivide(String input, int divisor) {
try {
int number = Integer.parseInt(input)
def result = number / divisor
println "$input / $divisor = $result"
} catch (NumberFormatException e) {
println "Error: '$input' is not a valid number"
} catch (ArithmeticException e) {
println "Error: Cannot divide $input by zero"
} catch (Exception e) {
println "Error: Something unexpected happened - ${e.message}"
}
}
parseAndDivide("100", 5)
parseAndDivide("abc", 5)
parseAndDivide("100", 0)
parseAndDivide(null, 5)
Output
100 / 5 = 20 Error: 'abc' is not a valid number Error: Cannot divide 100 by zero Error: Something unexpected happened - null
The last call passes null, which causes a NumberFormatException (since Integer.parseInt(null) throws that specific exception). The catch blocks are evaluated top to bottom – the first matching block wins. Always put more specific exception types before general ones, or the specific blocks will never execute.
Example 3: Multi-catch (Java 7 Style Pipe |)
Sometimes you want to handle multiple exception types in exactly the same way. Instead of duplicating code across separate catch blocks, you can use the pipe operator (|) to catch multiple types in a single block. This is the Java 7+ multi-catch syntax, and Groovy supports it fully.
Multi-catch with Pipe Operator
// Multi-catch: handling multiple exception types the same way
def processValue(value) {
try {
def list = [10, 20, 30]
int index = value as int
def result = list[index]
println "Value at index $index: $result"
// Force an operation that could fail
println "Doubled: ${result * 2}"
} catch (ClassCastException | IndexOutOfBoundsException | NullPointerException e) {
println "Input error (${e.class.simpleName}): ${e.message}"
}
}
processValue(1) // valid index
processValue("abc") // cannot cast to int
processValue(null) // null pointer
Output
Value at index 1: 20 Doubled: 40 Input error (ClassCastException): Cannot cast object 'abc' with class 'java.lang.String' to class 'int' Input error (NullPointerException): Cannot cast null to int
Multi-catch is purely about convenience and code cleanliness. It behaves identically to having separate catch blocks with the same body. Use it when the recovery logic is the same regardless of which specific exception was thrown.
Example 4: try-catch-finally
The finally block runs no matter what – whether the try block succeeds, throws an exception, or even if you use a return statement. This makes it the right place for cleanup code: closing files, releasing database connections, or unlocking resources.
try-catch-finally
// try-catch-finally: cleanup code that always runs
def readConfig(String filename) {
def reader = null
try {
println "Attempting to open: $filename"
reader = new File(filename).newReader()
def content = reader.text
println "Config loaded: ${content.take(50)}..."
return content
} catch (FileNotFoundException e) {
println "Config file not found: $filename"
println "Using default configuration"
return "default=true"
} catch (Exception e) {
println "Unexpected error reading config: ${e.message}"
return null
} finally {
if (reader != null) {
reader.close()
println "Reader closed successfully"
}
println "--- readConfig() finished ---"
}
}
// Test with a file that does not exist
def config = readConfig("/tmp/nonexistent-config.properties")
println "Config value: $config"
println()
// Test with a real temporary file
def tempFile = File.createTempFile("groovy-test-", ".properties")
tempFile.text = "app.name=GroovyCookbook\napp.version=5.0"
config = readConfig(tempFile.absolutePath)
println "Config value: ${config.take(40)}..."
tempFile.delete()
Output
Attempting to open: /tmp/nonexistent-config.properties Config file not found: /tmp/nonexistent-config.properties Using default configuration --- readConfig() finished --- Config value: default=true Attempting to open: /tmp/groovy-test-1234567890.properties Config loaded: app.name=GroovyCookbook app.version=5.0... Reader closed successfully --- readConfig() finished --- Config value: app.name=GroovyCookbook app.version=5.0...
Look at the output carefully. The finally block runs in both cases – when the file is missing and when it is read successfully. Even though the try block contains a return statement, the finally block still executes before the method returns. This is a guarantee from the JVM.
Example 5: try-with-resources (Groovy Style)
Java 7 introduced try-with-resources using the AutoCloseable interface. Groovy supports this, but it also provides a more idiomatic approach using the withCloseable and withReader methods from the GDK. These methods automatically close the resource when the closure finishes – even if an exception is thrown.
try-with-resources: Java Style vs Groovy Style
// try-with-resources: Java style vs Groovy idiom
// Create a temp file for testing
def tempFile = File.createTempFile("groovy-resources-", ".txt")
tempFile.text = "Line 1: Hello Groovy\nLine 2: Exception Handling\nLine 3: Resources"
// --- Java-style try-with-resources (works in Groovy) ---
println "=== Java Style ==="
try (def reader = new BufferedReader(new FileReader(tempFile))) {
reader.eachLine { line ->
println " $line"
}
}
// reader is automatically closed here
// --- Groovy idiomatic: withReader ---
println "\n=== Groovy withReader ==="
tempFile.withReader { reader ->
reader.eachLine { line ->
println " $line"
}
}
// reader is automatically closed here
// --- Groovy idiomatic: withCloseable ---
println "\n=== Groovy withCloseable ==="
new FileInputStream(tempFile).withCloseable { stream ->
println " Bytes available: ${stream.available()}"
def bytes = stream.readAllBytes()
println " Content length: ${bytes.length} bytes"
}
// stream is automatically closed here
// Clean up
tempFile.delete()
println "\nAll resources closed automatically!"
Output
=== Java Style === Line 1: Hello Groovy Line 2: Exception Handling Line 3: Resources === Groovy withReader === Line 1: Hello Groovy Line 2: Exception Handling Line 3: Resources === Groovy withCloseable === Bytes available: 60 Content length: 60 bytes All resources closed automatically!
The Groovy style (withReader, withCloseable, withWriter, withStream) is preferred in idiomatic Groovy code. It is shorter, eliminates the possibility of forgetting to close a resource, and reads naturally. Under the hood, these methods use try-finally to guarantee the resource is closed.
Example 6: Custom Exception Classes
Built-in exceptions like IllegalArgumentException or RuntimeException are fine for generic errors, but in real applications you often want custom exception types. They make your error handling more specific, carry domain-relevant information, and make it clear what went wrong.
Custom Exception Classes
// Custom exception classes with domain-specific context
class InsufficientFundsException extends RuntimeException {
BigDecimal balance
BigDecimal amount
InsufficientFundsException(BigDecimal balance, BigDecimal amount) {
super("Cannot withdraw \$${amount}: only \$${balance} available")
this.balance = balance
this.amount = amount
}
BigDecimal getShortfall() {
return amount - balance
}
}
class AccountLockedException extends RuntimeException {
String accountId
AccountLockedException(String accountId) {
super("Account $accountId is locked due to suspicious activity")
this.accountId = accountId
}
}
// A simple bank account class
class BankAccount {
String id
BigDecimal balance = 0.0
boolean locked = false
void withdraw(BigDecimal amount) {
if (locked) {
throw new AccountLockedException(id)
}
if (amount > balance) {
throw new InsufficientFundsException(balance, amount)
}
balance -= amount
println "Withdrew \$$amount. New balance: \$$balance"
}
}
// Test the custom exceptions
def account = new BankAccount(id: "ACC-001", balance: 100.00)
try {
account.withdraw(50.00)
account.withdraw(75.00) // This will fail
} catch (InsufficientFundsException e) {
println "Transaction failed: ${e.message}"
println "You are short by: \$${e.shortfall}"
}
println()
// Test locked account
def locked = new BankAccount(id: "ACC-002", balance: 500.00, locked: true)
try {
locked.withdraw(100.00)
} catch (AccountLockedException e) {
println "Access denied: ${e.message}"
println "Contact support with account: ${e.accountId}"
}
Output
Withdrew $50.00. New balance: $50.00 Transaction failed: Cannot withdraw $75.00: only $50.00 available You are short by: $25.00 Access denied: Account ACC-002 is locked due to suspicious activity Contact support with account: ACC-002
Custom exceptions make your error handling self-documenting. Instead of catching a generic RuntimeException and parsing the message string, you get typed access to relevant data like balance, amount, and shortfall. This is especially valuable in larger applications where exceptions cross module boundaries.
Example 7: Rethrowing Exceptions
Sometimes you want to catch an exception, do something with it (log it, update a counter), and then rethrow it so that the caller can handle it too. You can rethrow the original exception or throw a new one.
Rethrowing Exceptions
// Rethrowing exceptions: catch, log, and rethrow
class OrderService {
int failureCount = 0
def processOrder(Map order) {
try {
validateOrder(order)
println "Order ${order.id} processed successfully"
return true
} catch (IllegalArgumentException e) {
failureCount++
println "[LOG] Order validation failed (failure #${failureCount}): ${e.message}"
throw e // rethrow the original exception
}
}
private void validateOrder(Map order) {
if (!order.id) {
throw new IllegalArgumentException("Order ID is required")
}
if (!order.items || order.items.isEmpty()) {
throw new IllegalArgumentException("Order must have at least one item")
}
if (order.total <= 0) {
throw new IllegalArgumentException("Order total must be positive")
}
}
}
def service = new OrderService()
// Valid order
try {
service.processOrder([id: "ORD-001", items: ["Book"], total: 29.99])
} catch (Exception e) {
println "Caller caught: ${e.message}"
}
// Invalid order - exception is rethrown to the caller
try {
service.processOrder([id: "ORD-002", items: [], total: 15.00])
} catch (Exception e) {
println "Caller caught: ${e.message}"
}
// Another invalid order
try {
service.processOrder([id: null, items: ["Pen"], total: 5.00])
} catch (Exception e) {
println "Caller caught: ${e.message}"
}
println "\nTotal failures logged by service: ${service.failureCount}"
Output
Order ORD-001 processed successfully [LOG] Order validation failed (failure #1): Order must have at least one item Caller caught: Order must have at least one item [LOG] Order validation failed (failure #2): Order ID is required Caller caught: Order ID is required Total failures logged by service: 2
Rethrowing is common in service layers where you want to log or count errors but still let the caller decide what to do. Use throw e to rethrow the original exception with its original stack trace. If you create a new exception and throw that instead, you lose the original stack trace – unless you chain them (see the next example).
Example 8: Exception Chaining (cause)
Exception chaining lets you wrap a low-level exception inside a higher-level one, preserving the original cause. This is important when you want to present a clean, domain-specific error to the caller while keeping the technical details accessible for debugging.
Exception Chaining with cause
// Exception chaining: wrapping low-level exceptions
class DataLoadException extends RuntimeException {
DataLoadException(String message, Throwable cause) {
super(message, cause)
}
}
def loadUserData(String filename) {
try {
def file = new File(filename)
def text = file.text // throws FileNotFoundException
return text.split("\n").collect { line ->
def parts = line.split(",")
[name: parts[0], age: parts[1].toInteger()] // might throw NumberFormatException
}
} catch (FileNotFoundException e) {
throw new DataLoadException("Failed to load user data from '$filename'", e)
} catch (NumberFormatException e) {
throw new DataLoadException("Corrupt data in '$filename': invalid number format", e)
}
}
// Test 1: File not found - chained exception
try {
loadUserData("/tmp/nonexistent-users.csv")
} catch (DataLoadException e) {
println "Application error: ${e.message}"
println "Root cause: ${e.cause.class.simpleName} - ${e.cause.message}"
println()
}
// Test 2: Create a file with bad data - chained exception
def badFile = File.createTempFile("bad-users-", ".csv")
badFile.text = "Alice,30\nBob,not-a-number\nCharlie,25"
try {
loadUserData(badFile.absolutePath)
} catch (DataLoadException e) {
println "Application error: ${e.message}"
println "Root cause: ${e.cause.class.simpleName} - ${e.cause.message}"
}
badFile.delete()
Output
Application error: Failed to load user data from '/tmp/nonexistent-users.csv' Root cause: FileNotFoundException - /tmp/nonexistent-users.csv (No such file or directory) Application error: Corrupt data in '/tmp/bad-users-1234567890.csv': invalid number format Root cause: NumberFormatException - For input string: "not-a-number"
Exception chaining is a best practice in any layered application. The caller sees a clean DataLoadException with a clear message, while developers debugging the issue can drill into e.cause for the technical details. You can chain as many levels deep as needed – each exception’s cause points to the one that triggered it.
Example 9: Checked vs Unchecked – Groovy Treats All as Unchecked
This is one of Groovy’s most important differences from Java. In Java, checked exceptions (like IOException, SQLException, ClassNotFoundException) must be declared in a method’s throws clause or caught. In Groovy, the compiler completely ignores checked exceptions. You can throw and catch them freely without any declarations.
Checked vs Unchecked in Groovy
// In Groovy, all exceptions are unchecked - no throws declaration needed
// This would require "throws IOException" in Java, but not in Groovy
def readFile(String path) {
return new File(path).text
}
// This would require "throws ClassNotFoundException" in Java
def loadClass(String className) {
return Class.forName(className)
}
// This would require "throws InterruptedException" in Java
def waitABit() {
Thread.sleep(10)
println "Done waiting"
}
// You CAN still use throws if you want (for documentation)
def riskyOperation() throws IOException {
// The 'throws' clause is valid syntax but not enforced by the compiler
throw new IOException("Simulated I/O error")
}
// Test each method
println "=== Groovy ignores checked exceptions ==="
// No try-catch required - but it is still a good idea
try {
readFile("/tmp/this-does-not-exist.txt")
} catch (FileNotFoundException e) {
println "Caught FileNotFoundException: ${e.message}"
}
try {
loadClass("com.nonexistent.FakeClass")
} catch (ClassNotFoundException e) {
println "Caught ClassNotFoundException: ${e.message}"
}
waitABit()
try {
riskyOperation()
} catch (IOException e) {
println "Caught IOException: ${e.message}"
}
println "\nAll checked exceptions handled without 'throws' declarations!"
Output
=== Groovy ignores checked exceptions === Caught FileNotFoundException: /tmp/this-does-not-exist.txt (No such file or directory) Caught ClassNotFoundException: com.nonexistent.FakeClass Done waiting Caught IOException: Simulated I/O error All checked exceptions handled without 'throws' declarations!
This is a conscious design choice in Groovy. The Groovy team (and many in the Java community) consider checked exceptions to be a failed experiment – they add verbosity without significantly improving error handling. Groovy gives you the freedom to handle exceptions where it makes sense, without cluttering method signatures. That said, you should still handle exceptions where they can be meaningfully recovered – the fact that you can ignore them does not mean you should.
Example 10: Using assert for Validation vs Exceptions
Groovy’s assert statement throws a PowerAssertionError (a subclass of AssertionError) when the condition is false. While assertions are great for development and testing, they serve a different purpose than exceptions. Let us compare the two approaches.
Assert vs Exceptions for Validation
// Assert vs Exceptions: when to use which
// Approach 1: Using assert (good for development/testing)
def calculateDiscountWithAssert(BigDecimal price, int percentage) {
assert price > 0 : "Price must be positive, got: $price"
assert percentage in 0..100 : "Percentage must be 0-100, got: $percentage"
return price * (percentage / 100.0)
}
// Approach 2: Using exceptions (good for production APIs)
def calculateDiscountWithException(BigDecimal price, int percentage) {
if (price <= 0) {
throw new IllegalArgumentException("Price must be positive, got: $price")
}
if (percentage < 0 || percentage > 100) {
throw new IllegalArgumentException("Percentage must be 0-100, got: $percentage")
}
return price * (percentage / 100.0)
}
// Test valid input - both work the same
println "=== Valid Input ==="
println "Assert approach: ${calculateDiscountWithAssert(100.0, 20)}"
println "Exception approach: ${calculateDiscountWithException(100.0, 20)}"
// Test invalid input with assert
println "\n=== Assert with Invalid Input ==="
try {
calculateDiscountWithAssert(-50.0, 20)
} catch (AssertionError e) {
println "AssertionError: ${e.message}"
}
// Test invalid input with exception
println "\n=== Exception with Invalid Input ==="
try {
calculateDiscountWithException(100.0, 150)
} catch (IllegalArgumentException e) {
println "IllegalArgumentException: ${e.message}"
}
// The key difference: assert gives you Groovy Power Assert output
println "\n=== Power Assert in Action ==="
try {
def price = 0
assert price > 0 : "Price must be positive"
} catch (AssertionError e) {
println "Power assert output:\n${e.message}"
}
Output
=== Valid Input === Assert approach: 20.00 Exception approach: 20.00 === Assert with Invalid Input === AssertionError: Price must be positive, got: -50.0. Expression: (price > 0). Values: price = -50.0 === Exception with Invalid Input === IllegalArgumentException: Percentage must be 0-100, got: 150 === Power Assert in Action === Power assert output: Price must be positive. Expression: (price > 0). Values: price = 0
The rule of thumb: use assert for internal invariants during development and testing (things that should never happen if your code is correct). Use exceptions for validating external input in production APIs (things that will happen because users provide bad data). Assertions can be disabled at runtime via JVM flags, while exceptions cannot – so never use assert to validate user input in production. For more on Groovy’s powerful assert statement, see our Groovy Assert guide.
Example 11: Exception Handling in Closures
Closures are everywhere in Groovy – each, collect, findAll, withReader, and many more. Exception handling inside closures works the same as in regular methods, but there are some nuances worth knowing about.
Exception Handling in Closures
// Exception handling inside closures
// 1. Try-catch inside a closure
println "=== Try-catch Inside each() ==="
def numbers = ["10", "abc", "30", "xyz", "50"]
def parsed = []
numbers.each { item ->
try {
parsed << item.toInteger()
} catch (NumberFormatException e) {
println " Skipping invalid number: '$item'"
}
}
println "Parsed numbers: $parsed"
// 2. collect with error handling - return a default value on failure
println "\n=== Collect with Fallback ==="
def results = numbers.collect { item ->
try {
item.toInteger()
} catch (NumberFormatException e) {
-1 // default value for unparseable items
}
}
println "Results (with defaults): $results"
println "Valid only: ${results.findAll { it >= 0 }}"
// 3. Exception propagation from closures
println "\n=== Exception Propagating from Closure ==="
def processItems = { List items ->
items.each { item ->
if (item == null) {
throw new IllegalArgumentException("Null items are not allowed")
}
println " Processing: $item"
}
}
try {
processItems(["apple", "banana", null, "cherry"])
} catch (IllegalArgumentException e) {
println "Caught from closure: ${e.message}"
}
// 4. Safe transformation with collectEntries
println "\n=== Safe Parsing with collectEntries ==="
def rawConfig = ["port": "8080", "timeout": "not-a-number", "retries": "3"]
def config = rawConfig.collectEntries { key, value ->
try {
[(key): value.toInteger()]
} catch (NumberFormatException e) {
println " Warning: '$key' has invalid value '$value', using 0"
[(key): 0]
}
}
println "Parsed config: $config"
Output
=== Try-catch Inside each() === Skipping invalid number: 'abc' Skipping invalid number: 'xyz' Parsed numbers: [10, 30, 50] === Collect with Fallback === Results (with defaults): [10, -1, 30, -1, 50] Valid only: [10, 30, 50] === Exception Propagating from Closure === Processing: apple Processing: banana Caught from closure: Null items are not allowed === Safe Parsing with collectEntries === Warning: 'timeout' has invalid value 'not-a-number', using 0 Parsed config: [port:8080, timeout:0, retries:3]
Main points for closures: exceptions thrown inside a closure propagate up to the caller of the method that invokes the closure (e.g., each, collect). If you want to handle errors per-item without stopping iteration, put the try-catch inside the closure. If you want to stop iteration on the first error, let the exception propagate. For more on closures in general, see our Groovy Closures tutorial.
Example 12: Best Practices – When to Catch, When to Throw
This final example brings together the patterns from all the previous examples and demonstrates a real-world service with proper exception handling. It shows when to catch exceptions locally, when to rethrow them, when to wrap them, and when to let them propagate.
Real-World Exception Handling Strategy
// A complete exception handling strategy for a data pipeline
class PipelineException extends RuntimeException {
String stage
PipelineException(String stage, String message, Throwable cause = null) {
super("[$stage] $message", cause)
this.stage = stage
}
}
class DataPipeline {
// Stage 1: Read - catch and wrap I/O errors
List<String> readData(String source) {
try {
// Simulate reading from a file or API
if (source == "empty") return []
if (source == "error") throw new IOException("Connection refused")
return ["Alice,30", "Bob,25", "Charlie,35", "Diana,28"]
} catch (IOException e) {
throw new PipelineException("READ", "Failed to read from $source", e)
}
}
// Stage 2: Parse - handle individual record failures gracefully
List<Map> parseData(List<String> rawData) {
if (rawData.isEmpty()) {
throw new PipelineException("PARSE", "No data to parse")
}
def results = []
def errors = []
rawData.eachWithIndex { line, index ->
try {
def parts = line.split(",")
results << [name: parts[0].trim(), age: parts[1].trim().toInteger()]
} catch (Exception e) {
errors << "Line ${index + 1}: ${e.message}"
}
}
if (!errors.isEmpty()) {
println " Parse warnings: ${errors.join('; ')}"
}
return results
}
// Stage 3: Validate - throw on business rule violations
List<Map> validateData(List<Map> records) {
return records.findAll { record ->
if (record.age < 0 || record.age > 150) {
println " Validation: Skipping ${record.name} (invalid age: ${record.age})"
return false
}
return true
}
}
// Main pipeline: orchestrate stages with proper error handling
def runPipeline(String source) {
println "Pipeline starting for source: '$source'"
try {
def raw = readData(source)
def parsed = parseData(raw)
def validated = validateData(parsed)
println "Pipeline complete: ${validated.size()} records processed"
return validated
} catch (PipelineException e) {
println "Pipeline failed at stage [${e.stage}]: ${e.message}"
if (e.cause) {
println " Root cause: ${e.cause.class.simpleName} - ${e.cause.message}"
}
return []
}
}
}
// Run the pipeline with different scenarios
def pipeline = new DataPipeline()
println "=== Scenario 1: Happy Path ==="
def result = pipeline.runPipeline("users.csv")
result.each { println " ${it.name} (age ${it.age})" }
println "\n=== Scenario 2: Empty Source ==="
pipeline.runPipeline("empty")
println "\n=== Scenario 3: Read Error ==="
pipeline.runPipeline("error")
Output
=== Scenario 1: Happy Path === Pipeline starting for source: 'users.csv' Pipeline complete: 4 records processed Alice (age 30) Bob (age 25) Charlie (age 35) Diana (age 28) === Scenario 2: Empty Source === Pipeline starting for source: 'empty' Pipeline failed at stage [PARSE]: [PARSE] No data to parse === Scenario 3: Read Error === Pipeline starting for source: 'error' Pipeline failed at stage [READ]: [READ] Failed to read from error Root cause: IOException - Connection refused
This example demonstrates the three main strategies working together: (1) catch and wrap low-level exceptions into domain exceptions, (2) handle individual failures gracefully with per-record try-catch, and (3) let pipeline-level exceptions propagate to a single top-level handler. This is how production code should look – each layer handles what it can and passes the rest upward.
Best Practices for Groovy Exception Handling
After working through all 12 examples, let us consolidate what we have learned into actionable guidelines. These patterns come up repeatedly in production Groovy code, and following them consistently will save you hours of debugging.
Do’s and Don’ts
Here are the guidelines that will serve you well in real projects:
- Catch specific exceptions, not
Exception– CatchingExceptionorThrowablehides bugs. Catch the most specific type you can handle meaningfully. - Do not swallow exceptions silently – An empty catch block is almost always a bug. At minimum, log the error. If you intentionally ignore an exception, add a comment explaining why.
- Use Groovy’s resource management methods – Prefer
withReader,withWriter,withCloseableover manual try-finally for resource cleanup. - Create custom exceptions for your domain – Generic exceptions force callers to parse message strings. Custom exceptions carry structured data and enable precise catch blocks.
- Always chain exceptions – When wrapping a low-level exception, pass it as the
causeparameter. Losing the original stack trace makes debugging painful. - Use
assertfor invariants, exceptions for validation – Assertions are for things that should never happen. Exceptions are for things that will happen (bad input, network failures, missing files). - Handle exceptions at the right level – Catch exceptions where you can do something useful about them. If you can not recover, let them propagate to a higher-level handler.
- Log the stack trace, not just the message – In production,
e.messagealone is often not enough. Use a logging framework and log the full exception object. - Do not use exceptions for flow control – Throwing and catching exceptions is expensive. Use conditional logic (
if,switch) for expected cases, and exceptions for truly exceptional situations. - Remember: Groovy unchecked does not mean unhandled – Just because Groovy does not force you to catch checked exceptions does not mean you should ignore them. Think about what can go wrong and handle it.
Common Mistakes to Avoid
Even experienced developers make these mistakes. Here are the most common anti-patterns in Groovy exception handling and how to fix them.
Anti-Patterns to Avoid
// ❌ BAD: Swallowing exceptions silently
try {
riskyOperation()
} catch (Exception e) {
// nothing here - the error vanishes silently
}
// ✅ GOOD: At minimum, log the error
try {
riskyOperation()
} catch (Exception e) {
log.error("Operation failed", e)
}
// ❌ BAD: Catching Exception when you only expect IOException
try {
def content = new File("data.txt").text
} catch (Exception e) {
println "File not found" // What if it was a SecurityException?
}
// ✅ GOOD: Catch the specific exception type
try {
def content = new File("data.txt").text
} catch (FileNotFoundException e) {
println "File not found: ${e.message}"
}
// ❌ BAD: Using exceptions for flow control
def findUser(List users, String name) {
try {
users.each { user ->
if (user.name == name) throw new RuntimeException(user.toString())
}
} catch (RuntimeException e) {
return e.message // Abusing exceptions to break out of each()
}
}
// ✅ GOOD: Use find() instead
def findUserCorrect(List users, String name) {
return users.find { it.name == name }
}
// ❌ BAD: Losing the original exception cause
try {
loadData()
} catch (IOException e) {
throw new RuntimeException("Data load failed") // original cause is lost!
}
// ✅ GOOD: Chain the original exception
try {
loadData()
} catch (IOException e) {
throw new RuntimeException("Data load failed", e) // cause preserved
}
These anti-patterns are surprisingly common in real codebases. The silent catch block is the worst offender – it turns a loud, debuggable error into an invisible, hours-long mystery. The exception-for-flow-control pattern is a performance killer: creating an exception object, capturing a stack trace, and unwinding the call stack is orders of magnitude slower than a simple conditional check.
Exception Handling Strategy by Layer
In a well-structured application, each layer has a different responsibility when it comes to exceptions:
| Layer | Strategy | Example |
|---|---|---|
| Data Access | Catch low-level I/O and DB exceptions, wrap in domain exceptions | Catch SQLException, throw DataAccessException |
| Service / Business | Validate inputs, throw business exceptions, handle partial failures | Throw InsufficientFundsException, catch and retry transient errors |
| Controller / API | Catch all domain exceptions, map to HTTP status codes or error responses | Catch NotFoundException, return 404 |
| Top-level / Main | Global catch-all for unexpected errors, log and exit gracefully | Catch Exception, log stack trace, return error exit code |
This layered approach ensures that each exception is handled at the right level of abstraction. Low-level technical details do not leak to the user interface, and high-level business rules do not pollute the data access layer.
Conclusion
Groovy exception handling gives you the full power of Java’s try-catch-finally mechanism with less ceremony. All exceptions are unchecked, so your method signatures stay clean. Multi-catch blocks reduce duplication. The GDK provides idiomatic resource management with withCloseable and friends. And custom exception classes let you build a structured error handling strategy that scales with your application.
The 12 examples in this tutorial cover the most common patterns you will encounter: basic try-catch for simple error recovery, multiple catch blocks for handling different failure modes, finally blocks and try-with-resources for resource safety, custom exceptions for domain-specific errors, exception chaining for preserving context, and per-item error handling inside closures.
The most important takeaway is this: exception handling is not about preventing errors – it is about deciding what your program does when errors occur. Catch what you can handle, wrap what you cannot, and always preserve the original cause for debugging.
Summary
- All exceptions are unchecked in Groovy – no
throwsdeclarations required, but you should still handle exceptions intentionally - Multi-catch with
|reduces duplication when handling multiple exception types the same way - Use
withCloseableandwithReaderinstead of manual try-finally for resource management - Custom exceptions with structured fields are better than generic exceptions with parsed message strings
- Always chain exceptions using the
causeparameter to preserve debugging context - Use
assertfor development invariants, exceptions for production input validation - Exceptions in closures propagate to the calling method – catch inside the closure for per-item handling
If you also work with build tools, CI/CD pipelines, or cloud CLIs, check out Command Playground to practice 105+ CLI tools directly in your browser — no install needed.
Up next: Groovy Classes and Inheritance
Frequently Asked Questions
Does Groovy have checked exceptions?
No. Groovy treats all exceptions as unchecked. Unlike Java, the Groovy compiler does not require you to catch checked exceptions like IOException or SQLException, and you do not need to add throws declarations to your methods. You can still use throws for documentation purposes, but it has no effect on compilation. This is one of the key differences between Groovy and Java.
What is the difference between try-catch-finally and try-with-resources in Groovy?
try-catch-finally is the general-purpose exception handling mechanism where you manually write cleanup code in the finally block. try-with-resources (Java-style) automatically closes resources that implement AutoCloseable when the try block finishes. In Groovy, the idiomatic alternative is to use GDK methods like withCloseable, withReader, and withWriter, which handle resource cleanup automatically via closures.
How do I create a custom exception in Groovy?
Create a class that extends RuntimeException (or Exception). Add any domain-specific fields you need, and call super(message) or super(message, cause) in the constructor. For example: class OrderException extends RuntimeException { String orderId; OrderException(String id, String msg) { super(msg); this.orderId = id } }. Custom exceptions let callers catch specific error types and access structured error data instead of parsing message strings.
Should I use assert or exceptions for input validation in Groovy?
Use exceptions (throw new IllegalArgumentException(...)) for validating external input in production code, because assertions can be disabled at runtime via JVM flags. Use assert for internal invariants during development and testing – things that should never be false if your code is correct. Groovy’s power assertions provide excellent diagnostic output, making them ideal for test cases but inappropriate for production validation logic.
How do exceptions work inside Groovy closures?
Exceptions inside closures propagate up to the method that invokes the closure. For example, if a closure passed to each() throws an exception, it will stop the iteration and propagate to the code that called each(). If you want to handle errors per-item without stopping iteration, place the try-catch inside the closure body. If you want to stop processing on the first error, let the exception propagate naturally.
Related Posts
Previous in Series: Groovy findAll Method – Filter Collections Like a Pro
Next in Series: Groovy Classes and Inheritance
Related Topics You Might Like:
- Groovy Assert – Power Assertions
- Groovy Closures – Complete Guide
- Groovy CompileStatic and TypeChecked
This post is part of the Groovy Cookbook series on TechnoScripts.com

No comment