Update! PU31 has added the ability to throw managed exceptions. Read more about it here.
What I really miss in X++ is the ability to throw exception objects. If you throw an exception in X++, it’s just a number defining what kind of exception it is, which usually says just “Error” (Exception::Error). You also typically add a message to infolog, but the message in infolog and the exception don’t have any link.
Other object-oriented programming languages (such as Java, C# or Python) do it in a much better (object-oriented) way. You throw an object, which contains a lot of information about the error – the type (such as FileNotFoundException), a message, extra details such as the argument name of ArgumentNullException, a stack trace showing which sequence of calls led to the error and so on.
What I consider the most important is the type. This is necessary for meaningful recovery from errors. For example, you’ll go back to user when an error says that an input is invalid, while you may wait a while and try a request a bit later if you know that the error is about a network failure. If all you know is that there is an error, you can’t handle different errors in different ways; you can either stop execution when an error occurs or you’ll ignore all errors. Obviously, neither is ideal.
And other information besides type are is useful too. You may want to log stack trace of where the exception was thrown (not caught), you may want to know which parameter has a wrong value and so on.
I mentioned several times in previous blog posts that D365FO runs on the .NET (CLR) platform and you can catch exceptions objects of CLR exceptions (see Catching exceptions in AX 7). While this is useful, you can’t throw such exceptions from X++, therefore it alone can’t solve our problem.
But making it possible isn’t difficult. Let me show you my proof of concept.
What I wanted to achieve:
- Ability to throw “normal” .NET exceptions from X++. For example, I want to throw ArgumentNullException rather than just error(“Wrong parameters specified”).
- Ability to define and throw custom exceptions, specific to the business domain of D365FO.
- Ability to catch these custom exceptions in the usual way.
- Simple syntax for throwing these exceptions.
Throwing .NET exceptions from X++ can be easily done with a little C# class library. We can throw exception objects from C# and we can call C# methods from X++, therefore we can instantiate an exception, pass it to a C# method and throw it from there.
This is the class in C#:
public class ExceptionHelper { public static void ThrowException(Exception ex) { throw ex; } }
You could call it from X++ like this:
ExceptionHelper::ThrowException(new ArgumentNullException("_name");
While this works, it doesn’t look natural. I wanted something similar to throw error(“…”). We can’t throw exception objects with throw keyword in X++, but we can do this:
throw exception(new ArgumentNullException("_name"));
exception() is a global function calling the exception helper. It’s implemented in an extension of Global class:
[ExtensionOf(classStr(Global))] final static class Global_ManagedExceptions_Extension { public static Exception exception(System.Exception _ex) { Goshoom.DynamicsAX.ExceptionHelper::ThrowException(_ex); // The return statement is never called because an exception is thrown above, // but it makes the method compatible with the throw statement. return Exception::Error; } }
The fact that it returns Exception::Error makes it usable in the throw statement. Calling just exception(…) has the same effect as throw exception(…), but the latter is nicely consistent with throw error(…).
When we can throw exception objects, why should we limit ourselves to existing exception classes? We can easily define our own, specific to our needs in D365FO.
For demonstration, I’ve implemented FieldEmptyException, where you can provide information about the field and the record in question. Later you can use this information for logging, for highlighting failing records or anything you like.
For example, here I’m checking if a record has a value in the Email field and I throw a FieldEmptyException if not.
SysUserInfo user = ... if (!user.Email) { throw exception(new FieldEmptyException(fieldStr(SysUserInfo, Email), user)); }
Then we can catch FieldEmptyException and react to it, instead of catching all errors by the universal class catch (Exception::Error). We also have all details available when we catch the exception, as demonstrated here:
This infolog was generated by the following catch clause:
catch (fieldEmptyEx) { if (fieldEmptyEx.Record) { setPrefix("We can log all these details:"); info(strFmt("Exception type: %1", fieldEmptyEx.GetType().Name)); info(strFmt("Message: %1", fieldEmptyEx.Message)); info(strFmt("Table: %1", tableId2Name(fieldEmptyEx.Record.TableId))); info(strFmt("Field: %1", fieldEmptyEx.FieldName)); SysUserInfo user = fieldEmptyEx.Record as SysUserInfo; if (user) { info(strFmt("Data from the table: user ID %1, RecID %2", user.id, user.RecId)); } info(strFmt("Stack trace: %1", fieldEmptyEx.StackTrace)); } }
But if you want to use the usual catch (Exception::Error), you can! I’ve implemented FieldEmptyException as a specialization of ErrorException, therefore all logic working with the normal X++ errors still applies. This is important – you can start using these custom exceptions without worrying that they would stop being handled in existing code.
The complete source code (with examples) can be found on GitHub. It’s under MIT license, therefore you can do virtually anything with it, such as modifying it and including it in your commercial, closed-source solutions.
Hi Martin,
I’ve found similar function in standard SrsReportRunImpl.runReport(), probably managed exception can be raised without any external C# helper (didn’t test it):
using Microsoft.Dynamics.AX.Framework.Reporting.Shared;
System.Exception ex;
ReportExceptionHandler::Rethrow(ex);
I’m aware of this method. It’s similar, but not the same. It throws a new instance of System.Exception with the given exception in InnerException property. Which is a big problem – you can’t catch a specific type of exception if everything is wrapped in System.Exception.
So, basically it’s the same old Exception::CLRError, but with ability to set inner exception, right?