Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Exception Thrown From Service Not Being Caught in Controller

In my Grails service I have code like the following:

def createCharge(chargeParams) {
  try {
    def charge = Charge.create(chargeParams)
  } catch (CardException e) {
    throw e
  }
}

From my controller I do the following:

try  {
   service.createCharge(chargeParams)
} catch(CardException e) {

}

However, my controller is not catching the re-throwing of the CardException. If I wrap CardException in a RuntimeException via:

throw new RuntimeException(e)

and/or remove the signature from the catch to just catch(e) without typing it, it works, but I lose some information from the exception, like the message.

As a note, CardException is an Exception, not a RuntimeException. I'm not sure if that matters.

like image 951
Gregg Avatar asked Nov 14 '13 20:11

Gregg


People also ask

What happens if a thrown exception is not caught?

What happens if an exception is not caught? If an exception is not caught (with a catch block), the runtime system will abort the program (i.e. crash) and an exception message will print to the console.

Can you throw an exception without catching it?

You can avoid catching an exception, but if there is an exception thrown and you don't catch it your program will cease execution (crash). There is no way to ignore an exception. If your app doesn't need to do anything in response to a given exception, then you would simply catch it, and then do nothing.

What will happen when no catch handler is found for a thrown?

A function that "handles" that kind of exception catches it. In C++, when an exception is thrown, it cannot be ignored--there must be some kind of notification or termination of the program. If no user-provided exception handler is present, the compiler provides a default mechanism to terminate the program.

Why should type exception not be catched?

Because when you catch exception you're supposed to handle it properly. And you cannot expect to handle all kind of exceptions in your code. Also when you catch all exceptions, you may get an exception that cannot deal with and prevent code that is upper in the stack to handle it properly.


2 Answers

Unlike Java, you don't have to declare the (checked) exceptions that are thrown by a Groovy method, because any undeclared checked exceptions are wrapped in an UndeclaredThrowableException.

You seem to imply that Groovy wraps checked Exceptions by a UndeclaredThrowableException, this is not the case. If a Grails Service throws a unchecked Exception the Exception is eventually wrappped by an UndeclaredThrowableException but this is a java.lang.reflection mechanism and will only occur when a proxy class is involved.

This happens to be the case because a Grails Service is involved. I'm not sure how many proxy classes are exactly involved by there is at least one: a class that does the transaction handling (by Spring).

The class will wrap any method in the Service with a transaction and rollback the transaction when a RuntimeException occurs. Spring transaction management by default doesn't rollback in case of a checked Exception.

Java

This makes sense in plain old java, since the developer will see the Exception in the application code and will be warned to do something about it. If the developer is smart he will deal with any Exceptions during the scope of a transaction. If he doesn't rollback the transaction he is basically saying: “In this situation, the data integrity provided by the transaction is not important to me. I will recover from this error in some other way”

Groovy

This doesn't make sense in a Groovy world because the Groovy compiler doesn't enforce the dealing with Exceptions. It in effect treats Exceptions exactly the same way as RuntimeExceptions.

There is however a caveat: The reflection mechanism sees an Exception thrown by the proxy that is not in the method signature of the original Service. This is possible because:

  1. Groovy doesn't enforce handling the Exceptions
  2. A proxy method can always throw any Throwable (check InvocationHandler JavaDoc)

Because the reflection mechanism used is from Java, it must conform to the Java rules. So it will have to wrap the Exception in a RuntimeException,.. in this case a UndeclaredThrowableException.

Grails

Now it gets really tricky because if you call your Service method from a Controller and an Exception occurs. You will see a RuntimeException bubble up (because of some hidden mechanism) but your transaction will not be rolled back (because of some hidden mechanism).

This behaviour is very dangerous as the developer really has to remember to properly deal with any Exceptions (which the compiler will not help with) or the developer has to make sure that any Service is properly instructed with @Transactional(rollbackFor = Throwable).

This is a design issue that I think the developers of Grails have overlooked when they first designed this. But I think the default behaviour is so wrong and so dangerous this should really be changed.

like image 140
Timothy Kanters Avatar answered Oct 21 '22 17:10

Timothy Kanters


Unlike Java, you don't have to declare the (checked) exceptions that are thrown by a Groovy method, because any undeclared checked exceptions are wrapped in an UndeclaredThrowableException. So this:

def createCharge(chargeParams) {
  try {
    def charge = Charge.create(chargeParams)
  } catch (CardException e) {
    throw e
  }
}

is effectively the same as:

def createCharge(chargeParams) throws UndeclaredThrowableException {
  try {
    def charge = Charge.create(chargeParams)
  } catch (CardException e) {
    throw new UndeclaredThrowableException(e)
  }
}

the exception thrown by the above, obviously wouldn't be caught by:

try  {
   service.createCharge(chargeParams)
} catch(CardException e) {

}

But it will be caught by:

try  {
   service.createCharge(chargeParams)
} catch(e) {

}

Because this is just a shorthand for:

try  {
   service.createCharge(chargeParams)
} catch(Exception e) {

}
like image 32
Dónal Avatar answered Oct 21 '22 16:10

Dónal