diff --git a/src/language/utils/OFStream.cpp b/src/language/utils/OFStream.cpp
index 39b7cc8ecedee3ff76e3d6d54790d25f90d01569..ca7c77fad8a69fd79ae2b512635acbb7f03277fd 100644
--- a/src/language/utils/OFStream.cpp
+++ b/src/language/utils/OFStream.cpp
@@ -1,10 +1,12 @@
 #include <language/utils/OFStream.hpp>
 
+#include <utils/Filesystem.hpp>
 #include <utils/Messenger.hpp>
 
 OFStream::OFStream(const std::string& filename)
 {
   if (parallel::rank() == 0) {
+    createDirectoryIfNeeded(filename);
     m_fstream.open(filename);
     if (m_fstream.is_open()) {
       m_ostream = &m_fstream;
diff --git a/src/output/GnuplotWriter.cpp b/src/output/GnuplotWriter.cpp
index 50969799eeb4250b28d21722e8476ab86a88d9eb..8ccf7767180b30524ba54f7c4f085c3ad35783d2 100644
--- a/src/output/GnuplotWriter.cpp
+++ b/src/output/GnuplotWriter.cpp
@@ -5,6 +5,7 @@
 #include <mesh/Mesh.hpp>
 #include <mesh/MeshData.hpp>
 #include <mesh/MeshDataManager.hpp>
+#include <utils/Filesystem.hpp>
 #include <utils/Messenger.hpp>
 #include <utils/PugsTraits.hpp>
 #include <utils/RevisionInfo.hpp>
@@ -243,8 +244,16 @@ GnuplotWriter::_write(const MeshType& mesh,
                       const OutputNamedItemDataSet& output_named_item_data_set,
                       std::optional<double> time) const
 {
+  createDirectoryIfNeeded(_getFilename());
+
   if (parallel::rank() == 0) {
     std::ofstream fout{_getFilename()};
+    if (not fout) {
+      std::ostringstream error_msg;
+      error_msg << "cannot create file \"" << rang::fgB::yellow << _getFilename() << rang::fg::reset << '"';
+      throw NormalError(error_msg.str());
+    }
+
     fout.precision(15);
     fout.setf(std::ios_base::scientific);
 
@@ -286,6 +295,12 @@ GnuplotWriter::_write(const MeshType& mesh,
   for (size_t i_rank = 0; i_rank < parallel::size(); ++i_rank) {
     if (i_rank == parallel::rank()) {
       std::ofstream fout(_getFilename(), std::ios_base::app);
+      if (not fout) {
+        std::ostringstream error_msg;
+        error_msg << "cannot open file \"" << rang::fgB::yellow << _getFilename() << rang::fg::reset << '"';
+        throw NormalError(error_msg.str());
+      }
+
       fout.precision(15);
       fout.setf(std::ios_base::scientific);
 
diff --git a/src/output/GnuplotWriter1D.cpp b/src/output/GnuplotWriter1D.cpp
index a3156904843bdeb77be718de6a10614a3333afae..3e0740446e961505eb01e36eee5ba168e0f47dcc 100644
--- a/src/output/GnuplotWriter1D.cpp
+++ b/src/output/GnuplotWriter1D.cpp
@@ -5,6 +5,7 @@
 #include <mesh/Mesh.hpp>
 #include <mesh/MeshData.hpp>
 #include <mesh/MeshDataManager.hpp>
+#include <utils/Filesystem.hpp>
 #include <utils/Messenger.hpp>
 #include <utils/PugsTraits.hpp>
 #include <utils/RevisionInfo.hpp>
@@ -322,6 +323,12 @@ GnuplotWriter1D::_write(const MeshType& mesh,
 
   if (parallel::rank() == 0) {
     fout.open(_getFilename());
+    if (not fout) {
+      std::ostringstream error_msg;
+      error_msg << "cannot create file \"" << rang::fgB::yellow << _getFilename() << rang::fg::reset << '"';
+      throw NormalError(error_msg.str());
+    }
+
     fout.precision(15);
     fout.setf(std::ios_base::scientific);
     fout << _getDateAndVersionComment();
diff --git a/src/output/VTKWriter.cpp b/src/output/VTKWriter.cpp
index 0a40c7265e05a17d31e59bd41eeb10cb63935821..6ec6295e44b54bef9ea21c110120fdd873f71488 100644
--- a/src/output/VTKWriter.cpp
+++ b/src/output/VTKWriter.cpp
@@ -4,6 +4,7 @@
 #include <mesh/Mesh.hpp>
 #include <mesh/MeshData.hpp>
 #include <mesh/MeshDataManager.hpp>
+#include <utils/Filesystem.hpp>
 #include <utils/Messenger.hpp>
 #include <utils/RevisionInfo.hpp>
 #include <utils/Stringify.hpp>
@@ -364,8 +365,16 @@ VTKWriter::_write(const MeshType& mesh,
   output_named_item_data_set.add(NamedItemData{"cell_number", mesh.connectivity().cellNumber()});
   output_named_item_data_set.add(NamedItemData{"node_number", mesh.connectivity().nodeNumber()});
 
+  createDirectoryIfNeeded(m_base_filename);
+
   if (parallel::rank() == 0) {   // write PVTK file
     std::ofstream fout(_getFilenamePVTU());
+    if (not fout) {
+      std::ostringstream error_msg;
+      error_msg << "cannot create file \"" << rang::fgB::yellow << _getFilenamePVTU() << rang::fg::reset << '"';
+      throw NormalError(error_msg.str());
+    }
+
     fout << "<?xml version=\"1.0\"?>\n";
     fout << _getDateAndVersionComment();
     fout << "<VTKFile type=\"PUnstructuredGrid\">\n";
@@ -424,7 +433,7 @@ VTKWriter::_write(const MeshType& mesh,
     fout << "</PCellData>\n";
 
     for (size_t i_rank = 0; i_rank < parallel::size(); ++i_rank) {
-      fout << "<Piece Source=\"" << _getFilenameVTU(i_rank) << "\"/>\n";
+      fout << "<Piece Source=" << std::filesystem::path{_getFilenameVTU(i_rank)}.filename() << "/>\n";
     }
     fout << "</PUnstructuredGrid>\n";
     fout << "</VTKFile>\n";
@@ -435,6 +444,13 @@ VTKWriter::_write(const MeshType& mesh,
 
     // write VTK files
     std::ofstream fout(_getFilenameVTU(parallel::rank()));
+    if (not fout) {
+      std::ostringstream error_msg;
+      error_msg << "cannot create file \"" << rang::fgB::yellow << _getFilenameVTU(parallel::rank()) << rang::fg::reset
+                << '"';
+      throw NormalError(error_msg.str());
+    }
+
     fout << "<?xml version=\"1.0\"?>\n";
     fout << _getDateAndVersionComment();
     fout << "<VTKFile type=\"UnstructuredGrid\"";
@@ -632,7 +648,13 @@ VTKWriter::_write(const MeshType& mesh,
   }
 
   if (parallel::rank() == 0) {   // write PVD file
-    std::ofstream fout(m_base_filename + ".pvd");
+    const std::string pvd_filename = m_base_filename + ".pvd";
+    std::ofstream fout(pvd_filename);
+    if (not fout) {
+      std::ostringstream error_msg;
+      error_msg << "cannot create file \"" << rang::fgB::yellow << pvd_filename << rang::fg::reset << '"';
+      throw NormalError(error_msg.str());
+    }
 
     fout << "<?xml version=\"1.0\"?>\n";
     fout << _getDateAndVersionComment();
@@ -645,11 +667,13 @@ VTKWriter::_write(const MeshType& mesh,
         sout << m_base_filename;
         sout << '.' << std::setfill('0') << std::setw(4) << i_time << ".pvtu";
 
-        fout << "<DataSet timestep=\"" << m_period_manager->savedTime(i_time) << "\" file=\"" << sout.str() << "\"/>\n";
+        fout << "<DataSet timestep=\"" << m_period_manager->savedTime(i_time)
+             << "\" file=" << std::filesystem::path{sout.str()}.filename() << "/>\n";
       }
-      fout << "<DataSet timestep=\"" << *time << "\" file=\"" << _getFilenamePVTU() << "\"/>\n";
+      fout << "<DataSet timestep=\"" << *time << "\" file=" << std::filesystem::path{_getFilenamePVTU()}.filename()
+           << "/>\n";
     } else {
-      fout << "<DataSet file=\"" << _getFilenamePVTU() << "\"/>\n";
+      fout << "<DataSet file=" << std::filesystem::path{_getFilenamePVTU()}.filename() << "/>\n";
     }
 
     fout << "</Collection>\n";
diff --git a/src/utils/Filesystem.hpp b/src/utils/Filesystem.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..63af52b2e432f1baf55d2cbfd895485a408f7e10
--- /dev/null
+++ b/src/utils/Filesystem.hpp
@@ -0,0 +1,23 @@
+#ifndef FILESYSTEM_HPP
+#define FILESYSTEM_HPP
+
+#include <utils/Exceptions.hpp>
+#include <utils/PugsMacros.hpp>
+
+#include <filesystem>
+
+PUGS_INLINE void
+createDirectoryIfNeeded(const std::string& filename)
+{
+  std::filesystem::path path = std::filesystem::path{filename}.parent_path();
+  if (not path.empty()) {
+    try {
+      std::filesystem::create_directories(path);
+    }
+    catch (std::filesystem::filesystem_error& e) {
+      throw NormalError(e.what());
+    }
+  }
+}
+
+#endif   // FILESYSTEM_HPP
diff --git a/tests/test_OFStream.cpp b/tests/test_OFStream.cpp
index fbb5818dbc70451ff212c820edacfa595963e961..6ca7d98df0a6f36bf30add2b891a659beba757c2 100644
--- a/tests/test_OFStream.cpp
+++ b/tests/test_OFStream.cpp
@@ -13,7 +13,8 @@ TEST_CASE("OFStream", "[language]")
 {
   SECTION("ofstream")
   {
-    const std::string basename = std::filesystem::path{PUGS_BINARY_DIR}.append("tests").append("ofstream_");
+    const std::string basename =
+      std::filesystem::path{PUGS_BINARY_DIR}.append("tests").append("ofstream_dir").append("ofstream_");
     const std::string filename = basename + stringify(parallel::rank());
 
     // Ensures that the file is closed after this line
@@ -47,13 +48,4 @@ TEST_CASE("OFStream", "[language]")
 
     REQUIRE(not std::filesystem::exists(filename));
   }
-
-  SECTION("invalid filename")
-  {
-    if (parallel::rank() == 0) {
-      const std::string filename = "badpath/invalidpath/ofstream";
-
-      REQUIRE_THROWS_WITH(std::make_shared<OFStream>(filename), "error: cannot create file " + filename);
-    }
-  }
 }