In fact, Exceptions
are not value objects, but true class
instances, with some methods, potentially and a specific behavior within the
Delphi language. A Delphi exception
is something very specific,
and would not be easily converted into e.g. a JavaScript, Java or C#
exception.
Furthermore, re-creating and raising an instance of the same
exception
which occurred on the server side would induce a strong
dependency of the client code. For instance, if the server side raise a
ESQLDBOracle
exception, linking your client side with the whole
SynDBOracle.pas
unit just for exception would be a huge issue. The
ESQLDBOracle
exception, by itself, contains a link to an
Oracle statement instance, which would be lost when transmitted over
the wire. Some client platforms (e.g. mobile or AJAX) do not even have any
knowledge of what an Oracle database is...
As such, exception
are not good candidate on serialization, and
transmission per value, from the server side to the client side. We would NOT
be in favor of propagating exceptions to the client side.
This is why exceptions should better be intercepted on the server side, with
a try .. except
block within the service methods, then converted
into low level DTO types, specific to the service, then explicitly transmitted
as error codes to the client.
For instance, you may use an enumerate, in conjunction with a
variant for additional structured information (as a string or a more complex
TDocVariant
), to transmit an error to the client side.
See for instance how ICQRSQuery
, and its associated
TCQRSResult
enumeration, are defined in
mORMotDDD.pas
:
type TCQRSResult = (cqrsSuccess, cqrsSuccessWithMoreData, cqrsUnspecifiedError, cqrsBadRequest, cqrsNotFound, cqrsNoMoreData, cqrsDataLayerError, cqrsInternalError, cqrsDDDValidationFailed, cqrsInvalidContent, cqrsAlreadyExists, ...
ICQRSQuery = interface(IInvokable) ['{923614C8-A639-45AD-A3A3-4548337923C9}'] function GetLastError: TCQRSResult; function GetLastErrorInfo: variant; end;
The first cqrsSuccess
item of the TCQRSResult
enumerate will be the default one (mapped and transmitted to a 0 JSON number),
so in case of any stub of mock of the interfaces, methods will return as
successful, as expected - see Interfaces in practice: dependency injection,
stubs and mocks.
When any exception
is raised in a service method, a
TCQRSResult
enumeration value is returned as result, so that error
would be transmitted directly:
function TDDDMonitoredDaemon.Stop(out Information: variant): TCQRSResult; ... begin CqrsBeginMethod(qaNone,result); try .... CqrsSetResult(cqrsSuccess); except on E: Exception do CqrsSetResult(E,cqrsInternalError); end; end;
The mORMotDDD.pas
unit defines, in the TCQRSQueryObject
abstract class
,
some protected methods to handle errors and exceptions as expected by
ICQRSQuery
, e.g. the TCQRSQueryObject.CqrsSetResult()
method will set result := cqrsInternalError
and serialize the
E: Exception
within the internal variant used for additional
error, ready to be retrieved using
ICQRSQuery.GetLastErrorInfo
.
Exception
s are very useful to interrupt a process in case of a
catastrophic failure, but they are not the best method for transmitting errors
over remote services. Some newer languages (e.g. Google's
Go
), would even not define any exception types, but rely on
returned values, to transmit the errors. - see https://golang.org/doc/faq#exceptions
- in our client-server error handling design, we followed the same idea.
Feedback is welcome on our forum, as usual!