diff --git a/mex/sources/perfect_foresight_problem/DynamicModelCaller.cc b/mex/sources/perfect_foresight_problem/DynamicModelCaller.cc
index 4ed7b9be9ed2f8b99c8d8db1a5635f3c766edb1f..86c22c61dde40f4947f815d8bce67cb52900eff3 100644
--- a/mex/sources/perfect_foresight_problem/DynamicModelCaller.cc
+++ b/mex/sources/perfect_foresight_problem/DynamicModelCaller.cc
@@ -26,6 +26,7 @@
 using namespace std::literals::string_literals;
 
 std::string DynamicModelCaller::error_msg;
+std::string DynamicModelCaller::error_id;
 std::mutex DynamicModelCaller::error_mtx;
 
 #if !defined(_WIN32) && !defined(__CYGWIN32__)
@@ -40,6 +41,44 @@ DynamicModelDllCaller::dynamic_tt_fct DynamicModelDllCaller::residual_tt_fct {nu
 DynamicModelDllCaller::dynamic_fct DynamicModelDllCaller::residual_fct {nullptr},
     DynamicModelDllCaller::g1_fct {nullptr};
 
+void
+DynamicModelCaller::setErrMsg(std::string msg)
+{
+  std::lock_guard lk {error_mtx};
+  error_msg = move(msg);
+  error_id.clear();
+}
+
+void
+DynamicModelCaller::setMException(const mxArray* exception)
+{
+  const mxArray* message_mx {nullptr};
+  const mxArray* identifier_mx {nullptr};
+  if (mxIsClass(exception, "MException"))
+    {
+      message_mx = mxGetProperty(exception, 0, "message");
+      identifier_mx = mxGetProperty(exception, 0, "identifier");
+    }
+  else if (mxIsStruct(exception)) // For Octave
+    {
+      message_mx = mxGetField(exception, 0, "message");
+      identifier_mx = mxGetField(exception, 0, "identifier");
+    }
+  else
+    mexErrMsgTxt("Exception of incorrect type");
+
+  if (!message_mx || !mxIsChar(message_mx) || !identifier_mx || !mxIsChar(identifier_mx))
+    mexErrMsgTxt("Exception object malformed");
+
+  char* message = mxArrayToString(message_mx);
+  char* identifier = mxArrayToString(identifier_mx);
+  std::lock_guard lk {error_mtx};
+  error_msg = message;
+  error_id = identifier;
+  mxFree(message);
+  mxFree(identifier);
+}
+
 void
 DynamicModelDllCaller::load_dll(const std::string& basename)
 {
@@ -211,15 +250,13 @@ DynamicModelMatlabCaller::eval(double* resid)
                                               funcname.c_str())};
     if (exception)
       {
-        std::lock_guard lk {error_mtx};
-        error_msg = "An error occurred when calling " + funcname;
+        setMException(exception);
         return; // Avoid manipulating null pointers in plhs, see #1832
       }
 
     if (!mxIsDouble(plhs[0]) || mxIsSparse(plhs[0]))
       {
-        std::lock_guard lk {error_mtx};
-        error_msg = "Residuals should be a dense array of double floats";
+        setErrMsg("Residuals should be a dense array of double floats");
         return;
       }
 
@@ -252,8 +289,7 @@ DynamicModelMatlabCaller::eval(double* resid)
                                                 funcname.c_str())};
       if (exception)
         {
-          std::lock_guard lk {error_mtx};
-          error_msg = "An error occurred when calling " + funcname;
+          setMException(exception);
           return; // Avoid manipulating null pointers in plhs, see #1832
         }
 
@@ -265,8 +301,7 @@ DynamicModelMatlabCaller::eval(double* resid)
 
       if (!mxIsDouble(plhs[0]) || !mxIsSparse(plhs[0]))
         {
-          std::lock_guard lk {error_mtx};
-          error_msg = "Jacobian should be a sparse array of double floats";
+          setErrMsg("Residuals should be a dense array of double floats");
           return;
         }
 
diff --git a/mex/sources/perfect_foresight_problem/DynamicModelCaller.hh b/mex/sources/perfect_foresight_problem/DynamicModelCaller.hh
index 09612b4d69b4a28f4b200f57fd23a0e5da147dfa..33192afd33c5bad6528a0dc348ab3cd1651e05e7 100644
--- a/mex/sources/perfect_foresight_problem/DynamicModelCaller.hh
+++ b/mex/sources/perfect_foresight_problem/DynamicModelCaller.hh
@@ -48,6 +48,7 @@ public:
 
   // Used to store error messages (as exceptions cannot cross the OpenMP boundary)
   static std::string error_msg;
+  static std::string error_id;
   static std::mutex error_mtx; // Guard for access in OpenMP context
 
   DynamicModelCaller(bool linear_arg, bool compute_jacobian_arg) :
@@ -61,6 +62,8 @@ public:
      Only copies non-zero elements, according to g1_sparse_{rowval,colval,colptr}. */
   virtual void copy_jacobian_column(mwIndex col, double* dest) const = 0;
   virtual void eval(double* resid) = 0;
+  static void setErrMsg(std::string msg);
+  static void setMException(const mxArray* exception);
 };
 
 class DynamicModelDllCaller : public DynamicModelCaller
diff --git a/mex/sources/perfect_foresight_problem/perfect_foresight_problem.cc b/mex/sources/perfect_foresight_problem/perfect_foresight_problem.cc
index 7cd7252f2039fbfe8fb528c42ead0545f66ded73..177dedd8932bc8a436c7ec381b7fc76e299e31e5 100644
--- a/mex/sources/perfect_foresight_problem/perfect_foresight_problem.cc
+++ b/mex/sources/perfect_foresight_problem/perfect_foresight_problem.cc
@@ -1,5 +1,5 @@
 /*
- * Copyright © 2019-2024 Dynare Team
+ * Copyright © 2019-2025 Dynare Team
  *
  * This file is part of Dynare.
  *
@@ -205,6 +205,7 @@ mexFunction(int nlhs, mxArray* plhs[], int nrhs, const mxArray* prhs[])
     DynamicModelDllCaller::load_dll(basename);
 
   DynamicModelCaller::error_msg.clear();
+  DynamicModelCaller::error_id.clear();
 
   /* Parallelize the main loop, if use_dll and no external function (to avoid
      parallel calls to MATLAB) */
@@ -282,7 +283,7 @@ mexFunction(int nlhs, mxArray* plhs[], int nrhs, const mxArray* prhs[])
   /* Mimic a try/catch using a global string, since exceptions are not allowed
      to cross OpenMP boundary */
   if (!DynamicModelCaller::error_msg.empty())
-    mexErrMsgTxt(DynamicModelCaller::error_msg.c_str());
+    mexErrMsgIdAndTxt(DynamicModelCaller::error_id.c_str(), DynamicModelCaller::error_msg.c_str());
 
   if (compute_jacobian)
     {