diff --git a/smartmontools/ChangeLog b/smartmontools/ChangeLog
index b8673a5567aaf09f804584069f2abb152840dd96..68e79ff734d2982777ab28bc54f53cb4ca8d0e1d 100644
--- a/smartmontools/ChangeLog
+++ b/smartmontools/ChangeLog
@@ -1,5 +1,12 @@
 $Id$
 
+2023-05-29  Christian Franke  <franke@computer.org>
+
+	Add error messages for NVMe status values.
+	dev_interface.cpp: Add message to 'set_nvme_err()'.
+	nvmecmds.cpp, nvmecmds.h: Add functions 'nvme_status_*()'.
+	nvmeprint.cpp: Add message to '-l error' output (related: #1300).
+
 2023-03-21  Douglas Gilbert  <dgilbert@interlog.com>
 
 	tweak to suppress a warning from gcc/g++ 9.3
diff --git a/smartmontools/dev_interface.cpp b/smartmontools/dev_interface.cpp
index 4136e59532085f0a791eef267f668387e9802cfd..7771ed234d457c96a3e30386de58c40dfb21c730 100644
--- a/smartmontools/dev_interface.cpp
+++ b/smartmontools/dev_interface.cpp
@@ -14,6 +14,7 @@
 #include "dev_tunnelled.h"
 #include "atacmds.h" // ATA_SMART_CMD/STATUS
 #include "scsicmds.h" // scsi_cmnd_io
+#include "nvmecmds.h" // nvme_status_*()
 #include "utility.h"
 
 #include <errno.h>
@@ -229,12 +230,11 @@ bool scsi_device::scsi_pass_through_and_check(scsi_cmnd_io * iop,
 
 bool nvme_device::set_nvme_err(nvme_cmd_out & out, unsigned status, const char * msg /* = 0 */)
 {
-  if (!status)
-    throw std::logic_error("nvme_device: set_nvme_err() called with status=0");
-
   out.status = status;
   out.status_valid = true;
-  return set_err(EIO, "%sNVMe Status 0x%02x", (msg ? msg : ""), status);
+  char buf[64];
+  return set_err(nvme_status_to_errno(status), "%s%s (0x%03x)", (msg ? msg : ""),
+                 nvme_status_to_info_str(buf, status), status);
 }
 
 
diff --git a/smartmontools/nvmecmds.cpp b/smartmontools/nvmecmds.cpp
index 5ef37ed791feb1ec4df51b6031a9b6c19715ddfd..ee45a61ef90a15aaf4bcf21e1384a6bb6263044c 100644
--- a/smartmontools/nvmecmds.cpp
+++ b/smartmontools/nvmecmds.cpp
@@ -284,3 +284,221 @@ bool nvme_self_test(nvme_device * device, uint8_t stc, uint32_t nsid)
   in.cdw10 = stc;
   return nvme_pass_through(device, in);
 }
+
+// Return flagged error message for NVMe status SCT/SC fields or nullptr if unknown.
+// If message starts with '-', the status indicates an invalid command (EINVAL).
+static const char * nvme_status_to_flagged_str(uint16_t status)
+{
+  // Section 3.3.3.2.1 of NVM Express Base Specification Revision 2.0c, October 4, 2022
+  uint8_t sc = (uint8_t)status;
+  switch ((status >> 8) & 0x7) {
+    case 0x0: // Generic Command Status
+      if (sc < 0x80) switch (sc) {
+        case 0x00: return "Successful Completion";
+        case 0x01: return "-Invalid Command Opcode";
+        case 0x02: return "-Invalid Field in Command";
+        case 0x03: return "Command ID Conflict";
+        case 0x04: return "Data Transfer Error";
+        case 0x05: return "Commands Aborted due to Power Loss Notification";
+        case 0x06: return "Internal Error";
+        case 0x07: return "Command Abort Requested";
+        case 0x08: return "Command Aborted due to SQ Deletion";
+        case 0x09: return "Command Aborted due to Failed Fused Command";
+        case 0x0a: return "Command Aborted due to Missing Fused Command";
+        case 0x0b: return "-Invalid Namespace or Format";
+        case 0x0c: return "Command Sequence Error";
+        case 0x0d: return "-Invalid SGL Segment Descriptor";
+        case 0x0e: return "-Invalid Number of SGL Descriptors";
+        case 0x0f: return "-Data SGL Length Invalid";
+        case 0x10: return "-Metadata SGL Length Invalid";
+        case 0x11: return "-SGL Descriptor Type Invalid";
+        case 0x12: return "-Invalid Use of Controller Memory Buffer";
+        case 0x13: return "-PRP Offset Invalid";
+        case 0x14: return "Atomic Write Unit Exceeded";
+        case 0x15: return "Operation Denied";
+        case 0x16: return "-SGL Offset Invalid";
+        case 0x18: return "Host Identifier Inconsistent Format";
+        case 0x19: return "Keep Alive Timer Expired";
+        case 0x1a: return "-Keep Alive Timeout Invalid";
+        case 0x1b: return "Command Aborted due to Preempt and Abort";
+        case 0x1c: return "Sanitize Failed";
+        case 0x1d: return "Sanitize In Progress";
+        case 0x1e: return "SGL Data Block Granularity Invalid";
+        case 0x1f: return "Command Not Supported for Queue in CMB";
+        case 0x20: return "Namespace is Write Protected";
+        case 0x21: return "Command Interrupted";
+        case 0x22: return "Transient Transport Error";
+        case 0x23: return "Command Prohibited by Command and Feature Lockdown";
+        case 0x24: return "Admin Command Media Not Ready";
+        //   0x25-0x7f: Reserved
+      }
+      else switch (sc) {
+        //   0x80-0xbf: I/O Command Set Specific
+        case 0x80: return "LBA Out of Range";
+        case 0x81: return "Capacity Exceeded";
+        case 0x82: return "Namespace Not Ready";
+        case 0x83: return "Reservation Conflict";
+        case 0x84: return "Format In Progress";
+        case 0x85: return "-Invalid Value Size";
+        case 0x86: return "-Invalid Key Size";
+        case 0x87: return "KV Key Does Not Exist";
+        case 0x88: return "Unrecovered Error";
+        case 0x89: return "Key Exists";
+        //   0x90-0xbf: Reserved
+        //   0xc0-0xff: Vendor Specific
+      }
+      break;
+
+    case 0x1: // Command Specific Status
+      if (sc < 0x80) switch (sc) {
+        case 0x00: return "-Completion Queue Invalid";
+        case 0x01: return "-Invalid Queue Identifier";
+        case 0x02: return "-Invalid Queue Size";
+        case 0x03: return "Abort Command Limit Exceeded";
+        case 0x04: return "Abort Command Is Missing";
+        case 0x05: return "Asynchronous Event Request Limit Exceeded";
+        case 0x06: return "-Invalid Firmware Slot";
+        case 0x07: return "-Invalid Firmware Image";
+        case 0x08: return "-Invalid Interrupt Vector";
+        case 0x09: return "-Invalid Log Page";
+        case 0x0a: return "-Invalid Format";
+        case 0x0b: return "Firmware Activation Requires Conventional Reset";
+        case 0x0c: return "-Invalid Queue Deletion";
+        case 0x0d: return "Feature Identifier Not Saveable";
+        case 0x0e: return "Feature Not Changeable";
+        case 0x0f: return "Feature Not Namespace Specific";
+        case 0x10: return "Firmware Activation Requires NVM Subsystem Reset";
+        case 0x11: return "Firmware Activation Requires Controller Level Reset";
+        case 0x12: return "Firmware Activation Requires Maximum Time Violation";
+        case 0x13: return "Firmware Activation Prohibited";
+        case 0x14: return "Overlapping Range";
+        case 0x15: return "Namespace Insufficient Capacity";
+        case 0x16: return "-Namespace Identifier Unavailable";
+        case 0x18: return "Namespace Already Attached";
+        case 0x19: return "Namespace Is Private";
+        case 0x1a: return "Namespace Not Attached";
+        case 0x1b: return "Thin Provisioning Not Supported";
+        case 0x1c: return "-Controller List Invalid";
+        case 0x1d: return "Device Self-test In Progress";
+        case 0x1e: return "Boot Partition Write Prohibited";
+        case 0x1f: return "Invalid Controller Identifier";
+        case 0x20: return "-Invalid Secondary Controller State";
+        case 0x21: return "-Invalid Number of Controller Resources";
+        case 0x22: return "-Invalid Resource Identifier";
+        case 0x23: return "Sanitize Prohibited While Persistent Memory Region is Enabled";
+        case 0x24: return "-ANA Group Identifier Invalid";
+        case 0x25: return "ANA Attach Failed";
+        case 0x26: return "Insufficient Capacity";
+        case 0x27: return "Namespace Attachment Limit Exceeded";
+        case 0x28: return "Prohibition of Command Execution Not Supported";
+        case 0x29: return "I/O Command Set Not Supported";
+        case 0x2a: return "I/O Command Set Not Enabled";
+        case 0x2b: return "I/O Command Set Combination Rejected";
+        case 0x2c: return "-Invalid I/O Command Set";
+        case 0x2d: return "-Identifier Unavailable";
+        //   0x2e-0x6f: Reserved
+        //   0x70-0x7f: Directive Specific
+      }
+      else if (sc < 0xb8) switch (sc) {
+        //   0x80-0xbf: I/O Command Set Specific (overlap with Fabrics Command Set)
+        case 0x80: return "-Conflicting Attributes";
+        case 0x81: return "-Invalid Protection Information";
+        case 0x82: return "Attempted Write to Read Only Range";
+        case 0x83: return "Command Size Limit Exceeded";
+        //   0x84-0xb7: Reserved
+      }
+      else switch (sc) {
+        case 0xb8: return "Zoned Boundary Error";
+        case 0xb9: return "Zone Is Full";
+        case 0xba: return "Zone Is Read Only";
+        case 0xbb: return "Zone Is Offline";
+        case 0xbc: return "Zone Invalid Write";
+        case 0xbd: return "Too Many Active Zones";
+        case 0xbe: return "Too Many Open Zones";
+        case 0xbf: return "Invalid Zone State Transition";
+        //   0xc0-0xff: Vendor Specific
+      }
+      break;
+
+    case 0x2: // Media and Data Integrity Errors
+      switch (sc) {
+        //   0x00-0x7f: Reserved
+        case 0x80: return "Write Fault";
+        case 0x81: return "Unrecovered Read Error";
+        case 0x82: return "End-to-end Guard Check Error";
+        case 0x83: return "End-to-end Application Tag Check Error";
+        case 0x84: return "End-to-end Reference Tag Check Error";
+        case 0x85: return "Compare Failure";
+        case 0x86: return "Access Denied";
+        case 0x87: return "Deallocated or Unwritten Logical Block";
+        case 0x88: return "End-to-End Storage Tag Check Error";
+        //   0x89-0xbf: Reserved
+        //   0xc0-0xff: Vendor Specific
+      }
+      break;
+
+    case 0x3: // Path Related Status
+      switch (sc) {
+        case 0x00: return "Internal Path Error";
+        case 0x01: return "Asymmetric Access Persistent Loss";
+        case 0x02: return "Asymmetric Access Inaccessible";
+        case 0x03: return "Asymmetric Access Transition";
+        //   0x04-0x5f: Reserved
+        //   0x60-0x6f: Controller Detected Pathing Errors
+        case 0x60: return "Controller Pathing Error";
+        //   0x61-0x6f: Reserved
+        //   0x70-0x7f: Host Detected Pathing Errors
+        case 0x70: return "Host Pathing Error";
+        case 0x71: return "Command Aborted By Host";
+        //   0x72-0x7f: Reserved
+        //   0x80-0xbf: I/O Command Set Specific
+        //   0xc0-0xff: Vendor Specific
+      }
+      break;
+
+    //   0x4-0x6: Reserved
+    //   0x7: Vendor Specific
+  }
+  return nullptr;
+}
+
+// Return errno for NVMe status SCT/SC fields: 0, EINVAL or EIO.
+int nvme_status_to_errno(uint16_t status)
+{
+  if (!nvme_status_is_error(status))
+    return 0;
+  const char * s = nvme_status_to_flagged_str(status);
+  if (s && *s == '-')
+    return EINVAL;
+  return EIO;
+}
+
+// Return error message for NVMe status SCT/SC fields or nullptr if unknown.
+const char * nvme_status_to_str(uint16_t status)
+{
+  const char * s = nvme_status_to_flagged_str(status);
+  return (s && *s == '-' ? s + 1 : s);
+}
+
+// Return error message for NVMe status SCT/SC fields or explanatory message if unknown.
+const char * nvme_status_to_info_str(char * buf, size_t bufsize, uint16_t status)
+{
+  const char * s = nvme_status_to_str(status);
+  if (s)
+    return s;
+
+  uint8_t sct = (status >> 8) & 0x7, sc = (uint8_t)status;
+  const char * pfx = (sc >= 0xc0 ? "Vendor Specific " : "Unknown ");
+  switch (sct) {
+    case 0x0: s = "Generic Command Status"; break;
+    case 0x1: s = "Command Specific Status"; break;
+    case 0x2: s = "Media and Data Integrity Error"; break;
+    case 0x3: s = "Path Related Status"; break;
+    case 0x7: s = "Vendor Specific Status"; pfx = ""; break;
+  }
+  if (s)
+    snprintf(buf, bufsize, "%s%s 0x%02x", pfx, s, sc);
+  else
+    snprintf(buf, bufsize, "Unknown Status 0x%x/0x%02x", sct, sc);
+  return buf;
+}
diff --git a/smartmontools/nvmecmds.h b/smartmontools/nvmecmds.h
index 65208fc0ea7d9920a5f55915f9909ac0097c67ee..c48b6efddb634275989e0b8a707105cf82a7dea6 100644
--- a/smartmontools/nvmecmds.h
+++ b/smartmontools/nvmecmds.h
@@ -3,7 +3,7 @@
  *
  * Home page of code is: https://www.smartmontools.org
  *
- * Copyright (C) 2016-22 Christian Franke
+ * Copyright (C) 2016-23 Christian Franke
  *
  * Original code from <linux/nvme.h>:
  *   Copyright (C) 2011-2014 Intel Corporation
@@ -18,6 +18,8 @@
 
 #include "static_assert.h"
 
+#include <errno.h>
+#include <stddef.h>
 #include <stdint.h>
 
 // The code below was originally imported from <linux/nvme.h> include file from
@@ -278,4 +280,22 @@ bool nvme_read_self_test_log(nvme_device * device, uint32_t nsid,
 // Start Self-test
 bool nvme_self_test(nvme_device * device, uint8_t stc, uint32_t nsid);
 
+// Return true if NVMe status indicates an error.
+constexpr bool nvme_status_is_error(uint16_t status)
+  { return !!(status & 0x07ff); }
+
+// Return errno for NVMe status SCT/SC fields: 0, EINVAL or EIO.
+int nvme_status_to_errno(uint16_t status);
+
+// Return error message for NVMe status SCT/SC fields or nullptr if unknown.
+const char * nvme_status_to_str(uint16_t status);
+
+// Return error message for NVMe status SCT/SC fields or explanatory message if unknown.
+const char * nvme_status_to_info_str(char * buf, size_t bufsize, uint16_t status);
+
+// Version of above for fixed size buffers.
+template <size_t SIZE>
+inline const char * nvme_status_to_info_str(char (& buf)[SIZE], unsigned status)
+  { return nvme_status_to_info_str(buf, SIZE, status); }
+
 #endif // NVMECMDS_H
diff --git a/smartmontools/nvmeprint.cpp b/smartmontools/nvmeprint.cpp
index 88e1ba7689647d6cc0a0575756b410b16e2ed09c..13307895c200ae1744ae25bd0973af1a4ec6f245 100644
--- a/smartmontools/nvmeprint.cpp
+++ b/smartmontools/nvmeprint.cpp
@@ -3,7 +3,7 @@
  *
  * Home page of code is: https://www.smartmontools.org
  *
- * Copyright (C) 2016-22 Christian Franke
+ * Copyright (C) 2016-23 Christian Franke
  *
  * SPDX-License-Identifier: GPL-2.0-or-later
  */
@@ -449,7 +449,7 @@ static void print_error_log(const nvme_error_log_page * error_log,
     return;
   }
 
-  pout("Num   ErrCount  SQId   CmdId  Status  PELoc          LBA  NSID    VS\n");
+  pout("Num   ErrCount  SQId   CmdId  Status  PELoc          LBA  NSID    VS  Message\n");
   int unused = 0;
   for (unsigned i = 0; i < valid_entries; i++) {
     const nvme_error_log_page & e = error_log[i];
@@ -480,8 +480,10 @@ static void print_error_log(const nvme_error_log_page * error_log,
     if (e.vs != 0x00)
       snprintf(vs, sizeof(vs), "0x%02x", e.vs);
 
-    pout("%3u %10" PRIu64 " %5s %7s %7s %6s %12s %5s %5s\n",
-         i, e.error_count, sq, cm, st, pe, lb, ns, vs);
+    char buf[64];
+    pout("%3u %10" PRIu64 " %5s %7s %7s %6s %12s %5s %5s  %s\n",
+         i, e.error_count, sq, cm, st, pe, lb, ns, vs,
+         nvme_status_to_info_str(buf, e.status_field >> 1));
   }
 
   if (valid_entries == read_entries && read_entries < max_entries)