Exceptions

11 min read

10 min read

11 min read

10 min read

10 min read

9 min read

9 min read

10 min read

10 min read

Consider the following Slice definition.

Slice
struct TimeOfDay
{
short hour; // 0 - 23
short minute; // 0 - 59
short second; // 0 - 59
}
interface Clock
{
idempotent void setTime(TimeOfDay time);
}

There’s a potential problem: given that the TimeOfDay structure uses short as the type of each field, what will happen if a client invokes the setTime operation and passes a TimeOfDay value with meaningless field values, such as -199 for the minute field, or 42 for the hour? Obviously, it would be nice to provide some indication to the caller that this is meaningless. Slice allows you to define exceptions to indicate error conditions to the client. These Slice-defined exceptions are called user exceptions.

For example:

Slice
exception TimeException {} // Empty exceptions are legal
exception RangeException
{
TimeOfDay errorTime;
TimeOfDay minTime;
TimeOfDay maxTime;
}

A user exception is much like a structure in that it contains a number of fields. However, unlike structures, exceptions can have zero fields, that is, be empty. Like classes, user exceptions support inheritance and may include optional fields.

Exceptions allow you to return an arbitrary amount of error information to the client if an error condition arises in the implementation of an operation. Operations use an exception specification to indicate the exceptions that may be returned to the client:

Slice
interface Clock
{
idempotent TimeOfDay getTime();
idempotent void setTime(TimeOfDay time)
throws RangeException, TimeException;
}

This definition indicates that the setTime operation may throw either a RangeException or a TimeException exception (and no other type of exception). If the client receives a RangeException, the exception contains the TimeOfDay value that was passed to setTime and caused the error (in the errorTime field), as well as the minimum and maximum time values that can be used (in the minTime and maxTime fields). If setTime failed because of an error not caused by an illegal parameter value, it throws a TimeException. Obviously, because TimeException does not have fields, the client will have no idea what exactly it was that went wrong — it simply knows that the operation did not work.

To indicate that an operation does not throw any user exception, simply omit the exception specification. (There is no empty exception specification in Slice.)

The server-side Ice runtime does not verify that a user exception thrown by an operation is compatible with the exceptions listed in its Slice definition, although your implementation language may enforce its own restrictions. The Ice runtime in the client does validate user exceptions and throws UnknownUserException if it receives an unexpected user exception.

Exceptions are not first-class data types and first-class data types are not exceptions:

  • You cannot pass an exception as a parameter value.
  • You cannot use an exception as the type of a field.
  • You cannot use an exception as the element type of a sequence.
  • You cannot use an exception as the key or value type of a dictionary.
  • You cannot throw a value of non-exception type (such as a value of type int or string).

The reason for these restrictions is that some implementation languages use a specific and separate type for exceptions (in the same way as Slice does). For such languages, it would be difficult to map exceptions if they could be used as an ordinary data type.

Slice Exceptions support inheritance. For example:

Slice
exception BaseException
{
string reason;
}
enum RTError
{
DivideByZero, NegativeRoot, IllegalNull /* ... */
}
exception RuntimeException extends BaseException
{
RTError err;
}
enum LError { ValueOutOfRange, ValuesInconsistent, /* ... */ }
exception LogicException extends BaseException
{
LError err;
}
exception RangeException extends LogicException
{
TimeOfDay errorTime;
TimeOfDay minTime;
TimeOfDay maxTime;
}

These definitions set up a simple exception hierarchy:

  • BaseException is at the root of the tree and contains a string explaining the cause of the error.
  • Derived from BaseException are RuntimeException and LogicException. Each of these exceptions contains an enumerated value that further categorizes the error.
  • Finally, RangeException is derived from LogicException and reports the details of the specific error.

Setting up exception hierarchies such as this not only helps to create a more readable specification because errors are categorized, but also can be used at the language level to good advantage. For example, the Slice C++ mapping preserves the exception hierarchy so you can catch exceptions generically as a base exception, or set up exception handlers to deal with specific exceptions.

Note that, if the exception specification of an operation indicates a specific exception type, at runtime, the implementation of the operation may also throw more derived exceptions. For example:

Slice
exception BaseException
{
// ...
}
exception DerivedException extends BaseException
{
// ...
}
interface Example
{
// May throw BaseException or DerivedException
void op() throws BaseException;
}

In this example, op may throw a BaseException or a DerivedException exception, that is, any exception that is compatible with the exception types listed in the exception specification can be thrown at runtime.

As a system evolves, it is quite common for new, derived exceptions to be added to an existing hierarchy. Assume that we initially construct clients and server with the following definitions:

Slice
exception AppException
{
// ...
}
interface Application
{
void doSomething() throws AppException;
}

Also assume that a large number of clients are deployed in field, that is, when you upgrade the system, you cannot easily upgrade all the clients. As the application evolves, a new exception is added to the system and the server is redeployed with the new definition:

Slice
exception AppException
{
// ...
}
exception FatalApplicationException extends AppException
{
// ...
}
interface Application
{
void doSomething() throws AppException;
}

This raises the question of what should happen if the server throws a FatalApplicationException from doSomething. The answer depends whether the client was built using the old or the updated definition:

  • If the client was built using the same definition as the server, it simply receives a FatalApplicationException.
  • If the client was built with the original definition, that client has no knowledge that FatalApplicationException even exists. In this case, the Ice runtime automatically slices the exception to the most-derived type that is understood by the receiver (AppException, in this case) and discards the information that is specific to the derived part of the exception.

A Slice exception is mapped to a C++ class with the same name. This mapping is similar to the mapping of classes.

Consider the following Slice exceptions:

Slice
module M
{
exception GenericException
{
string reason;
}
exception BadTimeValException extends GenericException {}
}

The Slice compiler generates the following code for these exceptions:

C++
class GenericException : public Ice::UserException
{
public:
GenericException() noexcept = default;
GenericException(std::string reason) noexcept;
GenericException(const GenericException&) noexcept = default;
void ice_throw() const override;
std::string reason;
};
class BadTimeValException : public GenericException
{
public:
using GenericException::GenericException;
void ice_throw() const override;
};

There are a number of things to note about this generated code:

  1. The generated class GenericException inherits from Ice::UserException. Ice::UserException is the ultimate ancestor of all mapped exceptions. It derives indirectly from std::exception.
  2. The generated class contains a public data member for each Slice field.
  3. The generated class has a constructor that takes one argument for each data member, as well as a default constructor.
  4. The generated class as a noexcept copy-constructor, as required by C++ exception rules.
  5. The generated class has a virtual function, ice_throws. It is implemented by throwing *this.
  6. The generated class for BadTimeValException derives from the generated class GenericException.

You can print any user exception instance by calling ice_print on this instance. ice_print is defined on Ice::Exception. Alternatively, you can print an exception instance with operator<<:

C++
try
{
...
}
catch (const GreeterException& exception)
{
cout << "Caught " << exception << endl;
}

operator<< just calls Ice::Exception::ice_print.

You can use the metadata directive "cpp:custom-print" to tell the Slice compiler that you want to use your own custom print implementation. For example:

Slice
["cpp:custom-print"]
exception GreeterException { ... }

The Slice compiler then generates an ice_print override declaration in the mapped C++ class, and you are responsible to implement this member function.

A Slice exception is mapped to a C# class with the same name. This mapping is similar to the mapping of classes.

Consider the following Slice exceptions:

Slice
module M
{
exception GenericException
{
string reason;
}
exception BadTimeValException extends GenericException {}
}

The Slice compiler generates the following code for these exceptions:

C#
public partial class GenericException : Ice.UserException
{
public string reason = "";
public GenericException(string reason) { ... }
public GenericException() {}
}
public partial class BadTimeValException : GenericException
{
public BadTimeValException(string reason)
: base(reason)
{
}
public BadTimeValException() {}
}

There are a number of things to note about this generated code:

  1. The generated class GenericException inherits from Ice.UserException. Ice.UserException is the ultimate ancestor of all mapped exceptions. It derives indirectly from System.Exception.
  2. The generated class contains a public field for each Slice field.
  3. The generated class for BadTimeValException derives from the generated class GenericException.
  4. The generated class provides a primary constructor and a parameterless constructor; they are identical to the generated constructors for classes. See C# Mapping for Classes.

A Slice exception is mapped to a Java class with the same name. This mapping is similar to the mapping of classes.

Consider the following Slice exceptions:

Slice
module M
{
exception GenericException
{
string reason;
}
exception BadTimeValException extends GenericException {}
}

The Slice compiler generates the following code for these exceptions:

Java
public class GenericException extends com.zeroc.Ice.UserException {
public String reason;
public GenericException() {
this.reason = "";
}
public GenericException(String reason) {
this.reason = reason;
}
// ...
}
public class BadTimeValException extends GenericException {
public BadTimeValException() {
}
public BadTimeValException(String reason) {
super(reason);
}
// ...
}

There are a number of things to note about this generated code:

  1. The generated class GenericException inherits from UserException. UserException is the ultimate ancestor of all mapped exceptions. It’s a checked exception that derives from java.lang.Exception.
  2. The generated class contains a public field for each Slice field.
  3. The generated class for BadTimeValException derives from the generated class GenericException.
  4. The generated class provides a canonical constructor and a parameterless constructor; they are identical to the generated constructors for classes. See Java Mapping for Classes.

When an Slice operation has an exception specification, the corresponding client-side and server-side methods in Java have an exception specification. This is true for all mapped methods, except the proxy Async methods.

For example:

Slice
interface Greeter
{
["amd"]
string greet(string name) throws BadNameException, GoneFishingException;
}

maps to:

Java
// Client-side
public interface GreeterPrx extends com.zeroc.Ice.ObjectPrx {
default String greet(String name)
throws BadNameException, GoneFishingException {
...
}
default String greet(String name, java.util.Map<String, String> context)
throws BadNameException, GoneFishingException {
...
}
// No exception specification
default CompletableFuture<String> greetAsync(String name) {
...
}
default CompletableFuture<String> greetAsync(String name, java.util.Map<String, String> context) {
...
}
}
// Server-side
public interface Greeter extends com.zeroc.Ice.Object {
CompletionStage<String> greetAsync(String name, com.zeroc.Ice.Current current)
throws BadNameException, GoneFishingException;
}

A Slice exception is mapped to a JavaScript class with the same name. This mapping is similar to the mapping of JavaScript Mapping for Classes.

Consider the following Slice exceptions:

Slice
module M
{
exception GenericException
{
string reason;
}
exception BadTimeValException extends GenericException {}
}

The Slice compiler generates the following code for these exceptions:

JavaScript
class GenericException extends Ice.UserException {
constructor(reason = "") {
super();
this.reason = reason;
}
...
}
class BadTimeValException extends GenericException {}
TypeScript
class GenericException extends Ice.UserException {
constructor(reason?: string);
reason: string;
}
class BadTimeValException extends GenericException {}

There are a number of things to note about this generated code:

  1. The generated class GenericException inherits from Ice.UserException. Ice.UserException is the ultimate ancestor of all mapped exceptions. It derives indirectly from JavaScript Error type.
  2. The generated class contains a field for each Slice field.
  3. The generated class for BadTimeValException derives from the generated class GenericException.
  4. The generated class provides a constructor with a parameter for each field, just like mapped classes.

A Slice exception is mapped to a MATLAB class with the same name. This mapping is similar to the mapping of classes.

Consider the following Slice exceptions:

Slice
module M
{
exception GenericException
{
string reason;
}
exception BadTimeValException extends GenericException {}
}

The Slice compiler generates the following code for these exceptions:

MATLAB
classdef GenericException < Ice.UserException
properties
reason (1, :) char
end
methods
...
end
end
classdef BadTimeValException < M.GenericException
methods
...
end
end

There are a number of things to note about this generated code:

  1. The generated class GenericException inherits from Ice.UserException. Ice.UserException is the ultimate ancestor of all mapped exceptions. It derives indirectly from MException.
  2. The generated class contains a public property for each Slice field.
  3. The generated class for BadTimeValException derives from the generated class GenericException.
  4. The methods of the generated class are unimportant; in particular, since Ice for MATLAB is client-only, you don’t need to create user exceptions in MATLAB.

A Slice exception is mapped to a PHP class with the same name. This mapping is similar to the mapping of classes.

Consider the following Slice exceptions:

Slice
module M
{
exception GenericException
{
string reason;
}
exception BadTimeValException extends GenericException {}
}

The Slice compiler generates the following code for these exceptions:

PHP
class GenericException extends \Ice\UserException
{
public $reason;
// ...
}
class BadTimeValException extends \M\GenericException
{
// ..
}

There are a number of things to note about this generated code:

  1. The generated class GenericException inherits from \Ice\UserException. \Ice\UserException is the ultimate ancestor of all mapped exceptions. It derives indirectly from \Exception.
  2. The generated class contains a public variable for each Slice field.
  3. The generated class for BadTimeValException derives from the generated class GenericException.
  4. The methods of the generated class are unimportant; in particular, since Ice for PHP is client-only, you don’t need to create user exceptions in PHP.

A Slice exception is mapped to a Python class with the same name. This mapping is similar to the mapping of classes.

Consider the following Slice exceptions:

Slice
module M
{
exception GenericException
{
string reason;
}
exception BadTimeValException extends GenericException {}
}

The Slice compiler generates the following code for these exceptions:

Python
@dataclass
class GenericException(UserException):
reason: str = ""
@dataclass
class BadTimeValException(GenericException):
pass

There are a number of things to note about this generated code:

  1. The generated classes are dataclasses, just like the mapping for classes.
  2. The generate class GenericException inherits from UserException. Ice.UserException is the ultimate ancestor of all mapped exceptions. It derives indirectly from builtins.Exception.
  3. The generated class contains a public field for each Slice field.
  4. The generated class for BadTimeValException derives from the generated class GenericException.

A Slice exception is mapped to a Ruby class with the same name. This mapping is similar to the mapping of classes.

Consider the following Slice exceptions:

Slice
module M
{
exception GenericException
{
string reason;
}
exception BadTimeValException extends GenericException {}
}

The Slice compiler generates the following code for these exceptions:

Ruby
module ::M
class GenericException < Ice::UserException
attr_accessor :reason
# ...
end
class BadTimeValException < ::M::GenericException
# ...
end
end

There are a number of things to note about this generated code:

  1. The generated class GenericException inherits from Ice::UserException. Ice::UserException is the ultimate ancestor of all mapped exceptions. It derives indirectly from ::StandardError.
  2. For each Slice field, the generated class contains an instance variable and accessors to read and write this variable.
  3. The generated class for BadTimeValException derives from the generated class GenericException.
  4. The methods of the generated class are unimportant; in particular, since Ice for Ruby is client-only, you don’t need to create user exceptions in Ruby.

A Slice exception is mapped to a Swift class with the same name. This mapping is similar to the mapping of classes.

Consider the following Slice exceptions:

Slice
module M
{
exception GenericException
{
string reason;
}
exception BadTimeValException extends GenericException {}
}

The Slice compiler generates the following code for these exceptions:

Swift
open class GenericException: Ice.UserException, @unchecked Sendable {
public var reason: String = ""
public required init() {}
public init(reason: String) {
self.reason = reason
}
// ...
}
open class BadTimeValException: GenericException, @unchecked Sendable {
// ...
}

There are a number of things to note about this generated code:

  1. The generated class GenericException derives from Ice.UserException. The Ice.UserException class is the ultimate ancestor of all mapped exceptions. It conforms to the Error protocol.
  2. The generated class contains a public property for each Slice field.
  3. The generated class for BadTimeValException derives from the generated class GenericException.
  4. The generated class a provides default initializer and a memberwise initializer; they are identical to the generated initializers for Slice classes.