Repository: solid/community-server
Pull request #760 (2021-06-03 14:37:55)

Convert errors to Representations
Based on the discussion in #654 .

This doesn't solve that issue since I still need to add an ErrorToHtml converter, but that should be easy after these changes. Same for the `WwwAuthMetadataWriter` that would be needed for #655, which can use the status code that is now added to the metadata.

Adding those 2 things is the next thing I'll do, but this part could already be pushed since it stands on its own.

Some comments:
 * The IDP handler currently fixes the media type of the error response to text/plain since it doesn't have access to the preferences. Similarly, the LDP handler will do the same if there is an error in the RequestParser since it doesn't have the preferences yet at that point. This makes me wonder if perhaps we want to extract the preferences at an earlier point? We could also use our current preference parser in the IDP part, and perhaps even create a helper class/function to do preference extraction + error -> response + write response.
 * The reason for having a `TextErrorHandler` instead of a `ErrorToText` converter was because I wanted a solution that had the least chances of erroring as a backup in case something went wrong in the `ConvertingErrorHandler`.
 * I used this opportunity to rewrite the `AuthenticatedLdpHandler` unit tests since it was clear they were written when I had less of an idea on how to use jest.


Patch:
diff --git a/README.md b/README.md
index 3395f8be5..c622a2961 100644
--- a/README.md
+++ b/README.md
@@ -69,6 +69,7 @@ Additional recipes for configuring and deploying the server can be found at [sol
 | `--loggingLevel, -l` | `"info"`| |
 | `--rootFilePath, -f` | `"./"` | Folder to start the server in when using a file-based config. |
 | `--sparqlEndpoint, -s` | | Endpoint to call when using a SPARQL-based config. |
+| `--showStackTrace, -t` | false | Whether error stack traces should be shown in responses. |
 | `--podConfigJson` | `"./pod-config.json"` | JSON file to store pod configuration when using a dynamic config. |
 | `--idpTemplateFolder` | `"templates/idp"` | Folder containing the templates used for IDP interactions. |
 
diff --git a/config/example-https-file.json b/config/example-https-file.json
index 7c29e6be5..4d5915dc1 100644
--- a/config/example-https-file.json
+++ b/config/example-https-file.json
@@ -41,6 +41,7 @@
         "@id": "urn:solid-server:default:HttpServerFactory",
         "@type": "BaseHttpServerFactory",
         "handler": { "@id": "urn:solid-server:default:HttpHandler" },
+        "options_showStackTrace": { "@id": "urn:solid-server:default:variable:showStackTrace" },
         "options_https": true,
         "options_key": "/path/to/server.key",
         "options_cert": "/path/to/server.cert"
diff --git a/config/http/server-factory/https-example.json b/config/http/server-factory/https-example.json
index d3e069e2b..c668030ff 100644
--- a/config/http/server-factory/https-example.json
+++ b/config/http/server-factory/https-example.json
@@ -9,6 +9,7 @@
         "@id": "urn:solid-server:default:HttpServerFactory",
         "@type": "BaseHttpServerFactory",
         "handler": { "@id": "urn:solid-server:default:HttpHandler" },
+        "options_showStackTrace": { "@id": "urn:solid-server:default:variable:showStackTrace" },
         "options_https": true,
         "options_key": "/path/to/server.key",
         "options_cert": "/path/to/server.cert"
diff --git a/config/http/server-factory/no-websockets.json b/config/http/server-factory/no-websockets.json
index ca9a9296c..a64496a52 100644
--- a/config/http/server-factory/no-websockets.json
+++ b/config/http/server-factory/no-websockets.json
@@ -5,7 +5,8 @@
       "comment": "Creates a server that supports HTTP requests.",
       "@id": "urn:solid-server:default:ServerFactory",
       "@type": "BaseHttpServerFactory",
-      "handler": { "@id": "urn:solid-server:default:HttpHandler" }
+      "handler": { "@id": "urn:solid-server:default:HttpHandler" },
+      "options_showStackTrace": { "@id": "urn:solid-server:default:variable:showStackTrace" }
     }
   ]
 }
diff --git a/config/http/server-factory/websockets.json b/config/http/server-factory/websockets.json
index 77788d940..954b5d18d 100644
--- a/config/http/server-factory/websockets.json
+++ b/config/http/server-factory/websockets.json
@@ -8,7 +8,8 @@
       "baseServerFactory": {
         "@id": "urn:solid-server:default:HttpServerFactory",
         "@type": "BaseHttpServerFactory",
-        "handler": { "@id": "urn:solid-server:default:HttpHandler" }
+        "handler": { "@id": "urn:solid-server:default:HttpHandler" },
+        "options_showStackTrace": { "@id": "urn:solid-server:default:variable:showStackTrace" }
       },
       "webSocketHandler": {
         "@type": "UnsecureWebSocketsProtocol",
diff --git a/config/identity/handler/default.json b/config/identity/handler/default.json
index 42ece09c9..70731104f 100644
--- a/config/identity/handler/default.json
+++ b/config/identity/handler/default.json
@@ -21,7 +21,8 @@
       "providerFactory": { "@id": "urn:solid-server:default:IdentityProviderFactory" },
       "interactionPolicy": { "@id": "urn:solid-server:auth:password:AccountInteractionPolicy" },
       "interactionHttpHandler": { "@id": "urn:solid-server:auth:password:InteractionHttpHandler" },
-      "errorResponseWriter": { "@type": "ErrorResponseWriter" }
+      "errorHandler": { "@id": "urn:solid-server:default:ErrorHandler" },
+      "responseWriter": { "@id": "urn:solid-server:default:ResponseWriter" }
     }
   ]
 }
diff --git a/config/identity/handler/provider-factory/identity.json b/config/identity/handler/provider-factory/identity.json
index 1f021941a..c84487683 100644
--- a/config/identity/handler/provider-factory/identity.json
+++ b/config/identity/handler/provider-factory/identity.json
@@ -7,7 +7,8 @@
       "@type": "IdentityProviderFactory",
       "issuer": { "@id": "urn:solid-server:default:variable:baseUrl" },
       "configurationFactory": { "@id": "urn:solid-server:default:IdpConfigurationFactory" },
-      "errorResponseWriter": { "@type": "ErrorResponseWriter" }
+      "errorHandler": { "@id": "urn:solid-server:default:ErrorHandler" },
+      "responseWriter": { "@id": "urn:solid-server:default:ResponseWriter" }
     },
     {
       "comment": "Sets up the JWKS and cookie keys.",
diff --git a/config/ldp/handler/components/error-handler.json b/config/ldp/handler/components/error-handler.json
new file mode 100644
index 000000000..591ea950b
--- /dev/null
+++ b/config/ldp/handler/components/error-handler.json
@@ -0,0 +1,21 @@
+{
+  "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^0.0.0/components/context.jsonld",
+  "@graph": [
+    {
+      "comment": "Changes an error into a valid representation to send as a response.",
+      "@id": "urn:solid-server:default:ErrorHandler",
+      "@type": "WaterfallHandler",
+      "handlers": [
+        {
+          "@type": "ConvertingErrorHandler",
+          "converter": { "@id": "urn:solid-server:default:RepresentationConverter" },
+          "showStackTrace": { "@id": "urn:solid-server:default:variable:showStackTrace" }
+        },
+        {
+          "@type": "TextErrorHandler",
+          "showStackTrace": { "@id": "urn:solid-server:default:variable:showStackTrace" }
+        }
+      ]
+    }
+  ]
+}
diff --git a/config/ldp/handler/components/response-writer.json b/config/ldp/handler/components/response-writer.json
index d0fa526de..371f3426b 100644
--- a/config/ldp/handler/components/response-writer.json
+++ b/config/ldp/handler/components/response-writer.json
@@ -4,14 +4,8 @@
     {
       "comment": "Writes the result to the response stream.",
       "@id": "urn:solid-server:default:ResponseWriter",
-      "@type": "WaterfallHandler",
-      "handlers": [
-        { "@type": "ErrorResponseWriter" },
-        {
-          "@type": "BasicResponseWriter",
-          "metadataWriter": { "@id": "urn:solid-server:default:MetadataWriter" }
-        }
-      ]
+      "@type": "BasicResponseWriter",
+      "metadataWriter": { "@id": "urn:solid-server:default:MetadataWriter" }
     }
   ]
 }
diff --git a/config/ldp/handler/default.json b/config/ldp/handler/default.json
index 7411c5a12..430983de2 100644
--- a/config/ldp/handler/default.json
+++ b/config/ldp/handler/default.json
@@ -1,6 +1,7 @@
 {
   "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^0.0.0/components/context.jsonld",
   "import": [
+    "files-scs:config/ldp/handler/components/error-handler.json",
     "files-scs:config/ldp/handler/components/operation-handler.json",
     "files-scs:config/ldp/handler/components/request-parser.json",
     "files-scs:config/ldp/handler/components/response-writer.json"
@@ -15,6 +16,7 @@
       "args_permissionsExtractor": { "@id": "urn:solid-server:default:PermissionsExtractor" },
       "args_authorizer": { "@id": "urn:solid-server:default:Authorizer" },
       "args_operationHandler": { "@id": "urn:solid-server:default:OperationHandler" },
+      "args_errorHandler": { "@id": "urn:solid-server:default:ErrorHandler" },
       "args_responseWriter": { "@id": "urn:solid-server:default:ResponseWriter" }
     }
   ]
diff --git a/config/ldp/metadata-writer/writers/mapped.json b/config/ldp/metadata-writer/writers/mapped.json
index ea6ef7e51..74c3b1254 100644
--- a/config/ldp/metadata-writer/writers/mapped.json
+++ b/config/ldp/metadata-writer/writers/mapped.json
@@ -11,7 +11,7 @@
           "MappedMetadataWriter:_headerMap_value": "Content-Type"
         },
         {
-          "MappedMetadataWriter:_headerMap_key": "urn:solid:http:location",
+          "MappedMetadataWriter:_headerMap_key": "urn:npm:solid:community-server:http:location",
           "MappedMetadataWriter:_headerMap_value": "Location"
         }
       ]
diff --git a/config/util/representation-conversion/default.json b/config/util/representation-conversion/default.json
index c2e923d1c..8c7b2835c 100644
--- a/config/util/representation-conversion/default.json
+++ b/config/util/representation-conversion/default.json
@@ -23,7 +23,8 @@
           "@type": "ChainedConverter",
           "converters": [
             { "@id": "urn:solid-server:default:RdfToQuadConverter" },
-            { "@id": "urn:solid-server:default:QuadToRdfConverter" }
+            { "@id": "urn:solid-server:default:QuadToRdfConverter" },
+            { "@type": "ErrorToQuadConverter" }
           ]
         }
       ]
diff --git a/config/util/variables/default.json b/config/util/variables/default.json
index c54e43379..d7cc6e5aa 100644
--- a/config/util/variables/default.json
+++ b/config/util/variables/default.json
@@ -23,9 +23,15 @@
       "@type": "Variable"
     },
     {
+      "comment": "The max level of log messages that should be output.",
       "@id": "urn:solid-server:default:variable:loggingLevel",
       "@type": "Variable"
     },
+    {
+      "comment": "Whether error stack traces should be shown in the server responses.",
+      "@id": "urn:solid-server:default:variable:showStackTrace",
+      "@type": "Variable"
+    },
     {
       "comment": "Path to the JSON file used to store configuration for dynamic pods.",
       "@id": "urn:solid-server:default:variable:podConfigJson",
diff --git a/src/identity/IdentityProviderFactory.ts b/src/identity/IdentityProviderFactory.ts
index 8b1f795c2..35d2e5f52 100644
--- a/src/identity/IdentityProviderFactory.ts
+++ b/src/identity/IdentityProviderFactory.ts
@@ -6,8 +6,9 @@ import type { AnyObject,
   Account,
   ErrorOut } from 'oidc-provider';
 import { Provider } from 'oidc-provider';
+import type { ErrorHandler } from '../ldp/http/ErrorHandler';
 import type { ResponseWriter } from '../ldp/http/ResponseWriter';
-
+import type { RepresentationPreferences } from '../ldp/representation/RepresentationPreferences';
 import type { ConfigurationFactory } from './configuration/ConfigurationFactory';
 
 /**
@@ -18,13 +19,15 @@ import type { ConfigurationFactory } from './configuration/ConfigurationFactory'
 export class IdentityProviderFactory {
   private readonly issuer: string;
   private readonly configurationFactory: ConfigurationFactory;
-  private readonly errorResponseWriter: ResponseWriter;
+  private readonly errorHandler: ErrorHandler;
+  private readonly responseWriter: ResponseWriter;
 
   public constructor(issuer: string, configurationFactory: ConfigurationFactory,
-    errorResponseWriter: ResponseWriter) {
+    errorHandler: ErrorHandler, responseWriter: ResponseWriter) {
     this.issuer = issuer;
     this.configurationFactory = configurationFactory;
-    this.errorResponseWriter = errorResponseWriter;
+    this.errorHandler = errorHandler;
+    this.responseWriter = responseWriter;
   }
 
   public async createProvider(interactionPolicyOptions: {
@@ -81,7 +84,9 @@ export class IdentityProviderFactory {
       },
       renderError:
         async(ctx: KoaContextWithOIDC, out: ErrorOut, error: Error): Promise<void> => {
-          await this.errorResponseWriter.handleSafe({ response: ctx.res, result: error });
+          const preferences: RepresentationPreferences = { type: { 'text/plain': 1 }};
+          const result = await this.errorHandler.handleSafe({ error, preferences });
+          await this.responseWriter.handleSafe({ response: ctx.res, result });
         },
     };
     return new Provider(this.issuer, augmentedConfig);
diff --git a/src/identity/IdentityProviderHttpHandler.ts b/src/identity/IdentityProviderHttpHandler.ts
index 9fd421a8a..b3453599b 100644
--- a/src/identity/IdentityProviderHttpHandler.ts
+++ b/src/identity/IdentityProviderHttpHandler.ts
@@ -1,9 +1,11 @@
 import type { Provider } from 'oidc-provider';
+import type { ErrorHandler } from '../ldp/http/ErrorHandler';
 import type { ResponseWriter } from '../ldp/http/ResponseWriter';
+import type { RepresentationPreferences } from '../ldp/representation/RepresentationPreferences';
 import { getLoggerFor } from '../logging/LogUtil';
 import type { HttpHandlerInput } from '../server/HttpHandler';
 import { HttpHandler } from '../server/HttpHandler';
-import { isNativeError } from '../util/errors/ErrorUtil';
+import { assertNativeError, isNativeError } from '../util/errors/ErrorUtil';
 import type { IdentityProviderFactory } from './IdentityProviderFactory';
 import type { InteractionHttpHandler } from './interaction/InteractionHttpHandler';
 import type { InteractionPolicy } from './interaction/InteractionPolicy';
@@ -26,20 +28,23 @@ export class IdentityProviderHttpHandler extends HttpHandler {
   private readonly providerFactory: IdentityProviderFactory;
   private readonly interactionPolicy: InteractionPolicy;
   private readonly interactionHttpHandler: InteractionHttpHandler;
-  private readonly errorResponseWriter: ResponseWriter;
+  private readonly errorHandler: ErrorHandler;
+  private readonly responseWriter: ResponseWriter;
   private provider?: Provider;
 
   public constructor(
     providerFactory: IdentityProviderFactory,
     interactionPolicy: InteractionPolicy,
     interactionHttpHandler: InteractionHttpHandler,
-    errorResponseWriter: ResponseWriter,
+    errorHandler: ErrorHandler,
+    responseWriter: ResponseWriter,
   ) {
     super();
     this.providerFactory = providerFactory;
     this.interactionPolicy = interactionPolicy;
     this.interactionHttpHandler = interactionHttpHandler;
-    this.errorResponseWriter = errorResponseWriter;
+    this.errorHandler = errorHandler;
+    this.responseWriter = responseWriter;
   }
 
   /**
@@ -70,11 +75,10 @@ export class IdentityProviderHttpHandler extends HttpHandler {
     try {
       await this.interactionHttpHandler.handle({ ...input, provider });
     } catch (error: unknown) {
-      // ResponseWriter can only handle native errors
-      if (!isNativeError(error)) {
-        throw error;
-      }
-      await this.errorResponseWriter.handleSafe({ response: input.response, result: error });
+      assertNativeError(error);
+      const preferences: RepresentationPreferences = { type: { 'text/plain': 1 }};
+      const result = await this.errorHandler.handleSafe({ error, preferences });
+      await this.responseWriter.handleSafe({ response: input.response, result });
     }
   }
 }
diff --git a/src/index.ts b/src/index.ts
index 3142edeb0..6084c0141 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -109,7 +109,8 @@ export * from './ldp/http/AcceptPreferenceParser';
 export * from './ldp/http/BasicRequestParser';
 export * from './ldp/http/BasicResponseWriter';
 export * from './ldp/http/BodyParser';
-export * from './ldp/http/ErrorResponseWriter';
+export * from './ldp/http/ConvertingErrorHandler';
+export * from './ldp/http/ErrorHandler';
 export * from './ldp/http/OriginalUrlExtractor';
 export * from './ldp/http/Patch';
 export * from './ldp/http/PreferenceParser';
@@ -119,6 +120,7 @@ export * from './ldp/http/ResponseWriter';
 export * from './ldp/http/SparqlUpdateBodyParser';
 export * from './ldp/http/SparqlUpdatePatch';
 export * from './ldp/http/TargetExtractor';
+export * from './ldp/http/TextErrorHandler';
 
 // LDP/Operations
 export * from './ldp/operations/DeleteOperationHandler';
@@ -216,9 +218,10 @@ export * from './storage/accessors/SparqlDataAccessor';
 export * from './storage/conversion/ChainedConverter';
 export * from './storage/conversion/ConstantConverter';
 export * from './storage/conversion/ContentTypeReplacer';
+export * from './storage/conversion/ConversionUtil';
+export * from './storage/conversion/ErrorToQuadConverter';
 export * from './storage/conversion/IfNeededConverter';
 export * from './storage/conversion/PassthroughConverter';
-export * from './storage/conversion/ConversionUtil';
 export * from './storage/conversion/QuadToRdfConverter';
 export * from './storage/conversion/RdfToQuadConverter';
 export * from './storage/conversion/RepresentationConverter';
diff --git a/src/init/AppRunner.ts b/src/init/AppRunner.ts
index 3ab37f344..77401794c 100644
--- a/src/init/AppRunner.ts
+++ b/src/init/AppRunner.ts
@@ -44,41 +44,32 @@ export class AppRunner {
   } = {}): void {
     // Parse the command-line arguments
     const { argv: params } = yargs(argv.slice(2))
+      .strict()
       .usage('node ./bin/server.js [args]')
-      .check((args, options): boolean => {
-        // Only take flags as arguments, not filenames
-        if (args._ && args._.length > 0) {
-          throw new Error(`Unsupported arguments: ${args._.join('", "')}`);
+      .check((args): boolean => {
+        if (args._.length > 0) {
+          throw new Error(`Unsupported positional arguments: "${args._.join('", "')}"`);
         }
-        for (const key in args) {
-          // Skip filename arguments (_) and the script name ($0)
-          if (key !== '_' && key !== '$0') {
-            // Check if the argument occurs in the provided options list
-            if (!options[key]) {
-              throw new Error(`Unknown option: "${key}"`);
-            }
-            // Check if the argument actually has a value ('> ./bin/server.js -s' is not valid)
-            if (!args[key]) {
-              throw new Error(`Missing value for argument "${key}"`);
-            }
-            // Check if the argument only has 1 value
-            if (Array.isArray(args[key])) {
-              throw new Error(`Multiple values were provided for: "${key}", [${args[key]}]`);
-            }
+        for (const key of Object.keys(args)) {
+          // We have no options that allow for arrays
+          const val = args[key];
+          if (key !== '_' && Array.isArray(val)) {
+            throw new Error(`Multiple values were provided for: "${key}": "${val.join('", "')}"`);
           }
         }
         return true;
       })
       .options({
-        baseUrl: { type: 'string', alias: 'b' },
-        config: { type: 'string', alias: 'c' },
-        loggingLevel: { type: 'string', alias: 'l', default: 'info' },
-        mainModulePath: { type: 'string', alias: 'm' },
-        idpTemplateFolder: { type: 'string' },
-        port: { type: 'number', alias: 'p', default: 3000 },
-        rootFilePath: { type: 'string', alias: 'f', default: './' },
-        sparqlEndpoint: { type: 'string', alias: 's' },
-        podConfigJson: { type: 'string', default: './pod-config.json' },
+        baseUrl: { type: 'string', alias: 'b', requiresArg: true },
+        config: { type: 'string', alias: 'c', requiresArg: true },
+        loggingLevel: { type: 'string', alias: 'l', default: 'info', requiresArg: true },
+        mainModulePath: { type: 'string', alias: 'm', requiresArg: true },
+        idpTemplateFolder: { type: 'string', requiresArg: true },
+        port: { type: 'number', alias: 'p', default: 3000, requiresArg: true },
+        rootFilePath: { type: 'string', alias: 'f', default: './', requiresArg: true },
+        showStackTrace: { type: 'boolean', alias: 't', default: false },
+        sparqlEndpoint: { type: 'string', alias: 's', requiresArg: true },
+        podConfigJson: { type: 'string', default: './pod-config.json', requiresArg: true },
       })
       .help();
 
@@ -129,6 +120,7 @@ export class AppRunner {
       'urn:solid-server:default:variable:rootFilePath':
         this.resolveFilePath(params.rootFilePath),
       'urn:solid-server:default:variable:sparqlEndpoint': params.sparqlEndpoint,
+      'urn:solid-server:default:variable:showStackTrace': params.showStackTrace,
       'urn:solid-server:default:variable:podConfigJson':
         this.resolveFilePath(params.podConfigJson),
       'urn:solid-server:default:variable:idpTemplateFolder':
@@ -158,6 +150,7 @@ export interface ConfigVariables {
   baseUrl?: string;
   rootFilePath?: string;
   sparqlEndpoint?: string;
+  showStackTrace?: boolean;
   podConfigJson?: string;
   idpTemplateFolder?: string;
 }
diff --git a/src/ldp/AuthenticatedLdpHandler.ts b/src/ldp/AuthenticatedLdpHandler.ts
index 2c6f47002..dd0114597 100644
--- a/src/ldp/AuthenticatedLdpHandler.ts
+++ b/src/ldp/AuthenticatedLdpHandler.ts
@@ -6,7 +6,8 @@ import type { HttpHandlerInput } from '../server/HttpHandler';
 import { HttpHandler } from '../server/HttpHandler';
 import type { HttpRequest } from '../server/HttpRequest';
 import type { HttpResponse } from '../server/HttpResponse';
-import { isNativeError } from '../util/errors/ErrorUtil';
+import { assertNativeError } from '../util/errors/ErrorUtil';
+import type { ErrorHandler } from './http/ErrorHandler';
 import type { RequestParser } from './http/RequestParser';
 import type { ResponseDescription } from './http/response/ResponseDescription';
 import type { ResponseWriter } from './http/ResponseWriter';
@@ -14,6 +15,7 @@ import type { Operation } from './operations/Operation';
 import type { OperationHandler } from './operations/OperationHandler';
 import type { PermissionSet } from './permissions/PermissionSet';
 import type { PermissionsExtractor } from './permissions/PermissionsExtractor';
+import type { RepresentationPreferences } from './representation/RepresentationPreferences';
 
 /**
  * Collection of handlers needed for {@link AuthenticatedLdpHandler} to function.
@@ -39,6 +41,10 @@ export interface AuthenticatedLdpHandlerArgs {
    * Executed the operation.
    */
   operationHandler: OperationHandler;
+  /**
+   * Converts errors to a serializable format.
+   */
+  errorHandler: ErrorHandler;
   /**
    * Writes out the response of the operation.
    */
@@ -54,6 +60,7 @@ export class AuthenticatedLdpHandler extends HttpHandler {
   private readonly permissionsExtractor!: PermissionsExtractor;
   private readonly authorizer!: Authorizer;
   private readonly operationHandler!: OperationHandler;
+  private readonly errorHandler!: ErrorHandler;
   private readonly responseWriter!: ResponseWriter;
   private readonly logger = getLoggerFor(this);
 
@@ -91,16 +98,16 @@ export class AuthenticatedLdpHandler extends HttpHandler {
    * @returns A promise resolving when the handling is finished.
    */
   public async handle(input: HttpHandlerInput): Promise<void> {
-    let writeData: { response: HttpResponse; result: ResponseDescription | Error };
+    let writeData: { response: HttpResponse; result: ResponseDescription };
 
     try {
       writeData = { response: input.response, result: await this.runHandlers(input.request) };
     } catch (error: unknown) {
-      if (isNativeError(error)) {
-        writeData = { response: input.response, result: error };
-      } else {
-        throw error;
-      }
+      assertNativeError(error);
+      // We don't know the preferences yet at this point
+      const preferences: RepresentationPreferences = { type: { 'text/plain': 1 }};
+      const result = await this.errorHandler.handleSafe({ error, preferences });
+      writeData = { response: input.response, result };
     }
 
     await this.responseWriter.handleSafe(writeData);
@@ -119,6 +126,20 @@ export class AuthenticatedLdpHandler extends HttpHandler {
     const operation: Operation = await this.requestParser.handleSafe(request);
     this.logger.verbose(`Parsed ${operation.method} operation on ${operation.target.path}`);
 
+    try {
+      return await this.handleOperation(request, operation);
+    } catch (error: unknown) {
+      assertNativeError(error);
+      return await this.errorHandler.handleSafe({ error, preferences: operation.preferences });
+    }
+  }
+
+  /**
+   * Handles the operation object.
+   * Runs all non-RequestParser handlers.
+   * This way the preferences can be used in case an error needs to be written.
+   */
+  private async handleOperation(request: HttpRequest, operation: Operation): Promise<ResponseDescription> {
     const credentials: Credentials = await this.credentialsExtractor.handleSafe(request);
     this.logger.verbose(`Extracted credentials: ${credentials.webId}`);
 
diff --git a/src/ldp/http/ConvertingErrorHandler.ts b/src/ldp/http/ConvertingErrorHandler.ts
new file mode 100644
index 000000000..35d9ef50d
--- /dev/null
+++ b/src/ldp/http/ConvertingErrorHandler.ts
@@ -0,0 +1,94 @@
+import type {
+  RepresentationConverter,
+  RepresentationConverterArgs,
+} from '../../storage/conversion/RepresentationConverter';
+import { INTERNAL_ERROR } from '../../util/ContentTypes';
+import { getStatusCode } from '../../util/errors/ErrorUtil';
+import { toLiteral } from '../../util/TermUtil';
+import { HTTP, XSD } from '../../util/Vocabularies';
+import { BasicRepresentation } from '../representation/BasicRepresentation';
+import type { Representation } from '../representation/Representation';
+import { RepresentationMetadata } from '../representation/RepresentationMetadata';
+import type { ErrorHandlerArgs } from './ErrorHandler';
+import { ErrorHandler } from './ErrorHandler';
+import type { ResponseDescription } from './response/ResponseDescription';
+
+// Used by internal helper function
+type PreparedArguments = {
+  statusCode: number;
+  conversionArgs: RepresentationConverterArgs;
+};
+
+/**
+ * Converts an error into a Representation of content type internal/error.
+ * Then feeds that representation into its converter to create a representation based on the given preferences.
+ */
+export class ConvertingErrorHandler extends ErrorHandler {
+  private readonly converter: RepresentationConverter;
+  private readonly showStackTrace: boolean;
+
+  public constructor(converter: RepresentationConverter, showStackTrace = false) {
+    super();
+    this.converter = converter;
+    this.showStackTrace = showStackTrace;
+  }
+
+  public async canHandle(input: ErrorHandlerArgs): Promise<void> {
+    const { conversionArgs } = this.prepareArguments(input);
+
+    await this.converter.canHandle(conversionArgs);
+  }
+
+  public async handle(input: ErrorHandlerArgs): Promise<ResponseDescription> {
+    const { statusCode, conversionArgs } = this.prepareArguments(input);
+
+    const converted = await this.converter.handle(conversionArgs);
+
+    return this.createResponse(statusCode, converted);
+  }
+
+  public async handleSafe(input: ErrorHandlerArgs): Promise<ResponseDescription> {
+    const { statusCode, conversionArgs } = this.prepareArguments(input);
+
+    const converted = await this.converter.handleSafe(conversionArgs);
+
+    return this.createResponse(statusCode, converted);
+  }
+
+  /**
+   * Prepares the arguments used by all functions.
+   */
+  private prepareArguments({ error, preferences }: ErrorHandlerArgs): PreparedArguments {
+    const statusCode = getStatusCode(error);
+    const representation = this.toRepresentation(error, statusCode);
+    const identifier = { path: representation.metadata.identifier.value };
+    return { statusCode, conversionArgs: { identifier, representation, preferences }};
+  }
+
+  /**
+   * Creates a ResponseDescription based on the Representation.
+   */
+  private createResponse(statusCode: number, converted: Representation): ResponseDescription {
+    return {
+      statusCode,
+      metadata: converted.metadata,
+      data: converted.data,
+    };
+  }
+
+  /**
+   * Creates a Representation based on the given error.
+   * Content type will be internal/error.
+   * The status code is used for metadata.
+   */
+  private toRepresentation(error: Error, statusCode: number): Representation {
+    const metadata = new RepresentationMetadata(INTERNAL_ERROR);
+    metadata.add(HTTP.terms.statusCodeNumber, toLiteral(statusCode, XSD.terms.integer));
+
+    if (!this.showStackTrace) {
+      delete error.stack;
+    }
+
+    return new BasicRepresentation([ error ], metadata, false);
+  }
+}
diff --git a/src/ldp/http/ErrorHandler.ts b/src/ldp/http/ErrorHandler.ts
new file mode 100644
index 000000000..fa3dcc79b
--- /dev/null
+++ b/src/ldp/http/ErrorHandler.ts
@@ -0,0 +1,13 @@
+import { AsyncHandler } from '../../util/handlers/AsyncHandler';
+import type { RepresentationPreferences } from '../representation/RepresentationPreferences';
+import type { ResponseDescription } from './response/ResponseDescription';
+
+export interface ErrorHandlerArgs {
+  error: Error;
+  preferences: RepresentationPreferences;
+}
+
+/**
+ * Converts an error into a {@link ResponseDescription} based on the request preferences.
+ */
+export abstract class ErrorHandler extends AsyncHandler<ErrorHandlerArgs, ResponseDescription> {}
diff --git a/src/ldp/http/ErrorResponseWriter.ts b/src/ldp/http/ErrorResponseWriter.ts
deleted file mode 100644
index 5fe7970c5..000000000
--- a/src/ldp/http/ErrorResponseWriter.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { getLoggerFor } from '../../logging/LogUtil';
-import type { HttpResponse } from '../../server/HttpResponse';
-import { isNativeError } from '../../util/errors/ErrorUtil';
-import { HttpError } from '../../util/errors/HttpError';
-import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
-import type { ResponseDescription } from './response/ResponseDescription';
-import { ResponseWriter } from './ResponseWriter';
-
-/**
- * Writes to an {@link HttpResponse} based on the incoming Error.
- */
-export class ErrorResponseWriter extends ResponseWriter {
-  protected readonly logger = getLoggerFor(this);
-
-  public async canHandle(input: { response: HttpResponse; result: ResponseDescription | Error }): Promise<void> {
-    if (!isNativeError(input.result)) {
-      throw new NotImplementedHttpError('Only errors are supported');
-    }
-  }
-
-  public async handle(input: { response: HttpResponse; result: Error }): Promise<void> {
-    let code = 500;
-    if (HttpError.isInstance(input.result)) {
-      code = input.result.statusCode;
-    }
-    input.response.setHeader('content-type', 'text/plain');
-    input.response.writeHead(code);
-    input.response.end(typeof input.result.stack === 'string' ?
-      `${input.result.stack}\n` :
-      `${input.result.name}: ${input.result.message}\n`);
-  }
-}
diff --git a/src/ldp/http/ResponseWriter.ts b/src/ldp/http/ResponseWriter.ts
index 926c320c5..593c0b184 100644
--- a/src/ldp/http/ResponseWriter.ts
+++ b/src/ldp/http/ResponseWriter.ts
@@ -3,8 +3,7 @@ import { AsyncHandler } from '../../util/handlers/AsyncHandler';
 import type { ResponseDescription } from './response/ResponseDescription';
 
 /**
- * Writes to the HttpResponse.
- * Response depends on the operation result and potentially which errors was thrown.
+ * Writes the ResponseDescription to the HttpResponse.
  */
 export abstract class ResponseWriter
-  extends AsyncHandler<{ response: HttpResponse; result: ResponseDescription | Error }> {}
+  extends AsyncHandler<{ response: HttpResponse; result: ResponseDescription }> {}
diff --git a/src/ldp/http/TextErrorHandler.ts b/src/ldp/http/TextErrorHandler.ts
new file mode 100644
index 000000000..d93363a93
--- /dev/null
+++ b/src/ldp/http/TextErrorHandler.ts
@@ -0,0 +1,37 @@
+import { getStatusCode } from '../../util/errors/ErrorUtil';
+import { guardedStreamFrom } from '../../util/StreamUtil';
+import { toLiteral } from '../../util/TermUtil';
+import { HTTP, XSD } from '../../util/Vocabularies';
+import { RepresentationMetadata } from '../representation/RepresentationMetadata';
+import type { ErrorHandlerArgs } from './ErrorHandler';
+import { ErrorHandler } from './ErrorHandler';
+import type { ResponseDescription } from './response/ResponseDescription';
+
+/**
+ * Returns a simple text description of an error.
+ * This class is mostly a failsafe in case all other solutions fail.
+ */
+export class TextErrorHandler extends ErrorHandler {
+  private readonly showStackTrace: boolean;
+
+  public constructor(showStackTrace = false) {
+    super();
+    this.showStackTrace = showStackTrace;
+  }
+
+  public async handle({ error }: ErrorHandlerArgs): Promise<ResponseDescription> {
+    const statusCode = getStatusCode(error);
+    const metadata = new RepresentationMetadata('text/plain');
+    metadata.add(HTTP.terms.statusCodeNumber, toLiteral(statusCode, XSD.terms.integer));
+
+    const text = typeof error.stack === 'string' && this.showStackTrace ?
+      `${error.stack}\n` :
+      `${error.name}: ${error.message}\n`;
+
+    return {
+      statusCode,
+      metadata,
+      data: guardedStreamFrom(text),
+    };
+  }
+}
diff --git a/src/ldp/http/metadata/SlugParser.ts b/src/ldp/http/metadata/SlugParser.ts
index 6324981a8..16e30422d 100644
--- a/src/ldp/http/metadata/SlugParser.ts
+++ b/src/ldp/http/metadata/SlugParser.ts
@@ -1,7 +1,7 @@
 import { getLoggerFor } from '../../../logging/LogUtil';
 import type { HttpRequest } from '../../../server/HttpRequest';
 import { BadRequestHttpError } from '../../../util/errors/BadRequestHttpError';
-import { HTTP } from '../../../util/Vocabularies';
+import { SOLID_HTTP } from '../../../util/Vocabularies';
 import type { RepresentationMetadata } from '../../representation/RepresentationMetadata';
 import { MetadataParser } from './MetadataParser';
 
@@ -19,7 +19,7 @@ export class SlugParser extends MetadataParser {
         throw new BadRequestHttpError('Request has multiple Slug headers');
       }
       this.logger.debug(`Request Slug is '${slug}'.`);
-      input.metadata.set(HTTP.slug, slug);
+      input.metadata.set(SOLID_HTTP.slug, slug);
     }
   }
 }
diff --git a/src/ldp/http/response/CreatedResponseDescription.ts b/src/ldp/http/response/CreatedResponseDescription.ts
index a8603e1a3..7d35ac75d 100644
--- a/src/ldp/http/response/CreatedResponseDescription.ts
+++ b/src/ldp/http/response/CreatedResponseDescription.ts
@@ -1,5 +1,5 @@
 import { DataFactory } from 'n3';
-import { HTTP } from '../../../util/Vocabularies';
+import { SOLID_HTTP } from '../../../util/Vocabularies';
 import { RepresentationMetadata } from '../../representation/RepresentationMetadata';
 import type { ResourceIdentifier } from '../../representation/ResourceIdentifier';
 import { ResponseDescription } from './ResponseDescription';
@@ -9,7 +9,7 @@ import { ResponseDescription } from './ResponseDescription';
  */
 export class CreatedResponseDescription extends ResponseDescription {
   public constructor(location: ResourceIdentifier) {
-    const metadata = new RepresentationMetadata({ [HTTP.location]: DataFactory.namedNode(location.path) });
+    const metadata = new RepresentationMetadata({ [SOLID_HTTP.location]: DataFactory.namedNode(location.path) });
     super(201, metadata);
   }
 }
diff --git a/src/server/BaseHttpServerFactory.ts b/src/server/BaseHttpServerFactory.ts
index 8c02f89b5..3b0cd048d 100644
--- a/src/server/BaseHttpServerFactory.ts
+++ b/src/server/BaseHttpServerFactory.ts
@@ -21,6 +21,11 @@ export interface BaseHttpServerOptions {
    */
   https?: boolean;
 
+  /**
+   * If the error stack traces should be shown in case the HttpHandler throws one.
+   */
+  showStackTrace?: boolean;
+
   key?: string;
   cert?: string;
 
@@ -61,7 +66,14 @@ export class BaseHttpServerFactory implements HttpServerFactory {
           this.logger.info(`Received ${request.method} request for ${request.url}`);
           await this.handler.handleSafe({ request: guardStream(request), response });
         } catch (error: unknown) {
-          const errMsg = isNativeError(error) ? `${error.name}: ${error.message}\n${error.stack}` : 'Unknown error.';
+          let errMsg: string;
+          if (!isNativeError(error)) {
+            errMsg = 'Unknown error.\n';
+          } else if (this.options.showStackTrace && error.stack) {
+            errMsg = `${error.stack}\n`;
+          } else {
+            errMsg = `${error.name}: ${error.message}\n`;
+          }
           this.logger.error(errMsg);
           if (response.headersSent) {
             response.end();
diff --git a/src/storage/DataAccessorBasedStore.ts b/src/storage/DataAccessorBasedStore.ts
index 5df621a50..439b0713c 100644
--- a/src/storage/DataAccessorBasedStore.ts
+++ b/src/storage/DataAccessorBasedStore.ts
@@ -26,7 +26,7 @@ import {
 } from '../util/PathUtil';
 import { parseQuads } from '../util/QuadUtil';
 import { addResourceMetadata } from '../util/ResourceUtil';
-import { CONTENT_TYPE, DC, HTTP, LDP, POSIX, PIM, RDF, VANN, XSD } from '../util/Vocabularies';
+import { CONTENT_TYPE, DC, SOLID_HTTP, LDP, POSIX, PIM, RDF, VANN, XSD } from '../util/Vocabularies';
 import type { DataAccessor } from './accessors/DataAccessor';
 import type { ResourceStore } from './ResourceStore';
 
@@ -407,8 +407,8 @@ export class DataAccessorBasedStore implements ResourceStore {
   Promise<ResourceIdentifier> {
     // Get all values needed for naming the resource
     const isContainer = this.isNewContainer(metadata);
-    const slug = metadata.get(HTTP.slug)?.value;
-    metadata.removeAll(HTTP.slug);
+    const slug = metadata.get(SOLID_HTTP.slug)?.value;
+    metadata.removeAll(SOLID_HTTP.slug);
 
     let newID: ResourceIdentifier = this.createURI(container, isContainer, slug);
 
@@ -439,7 +439,7 @@ export class DataAccessorBasedStore implements ResourceStore {
     if (this.hasContainerType(metadata.getAll(RDF.type))) {
       return true;
     }
-    const slug = suffix ?? metadata.get(HTTP.slug)?.value;
+    const slug = suffix ?? metadata.get(SOLID_HTTP.slug)?.value;
     return Boolean(slug && isContainerPath(slug));
   }
 
diff --git a/src/storage/conversion/ErrorToQuadConverter.ts b/src/storage/conversion/ErrorToQuadConverter.ts
new file mode 100644
index 000000000..9591c236b
--- /dev/null
+++ b/src/storage/conversion/ErrorToQuadConverter.ts
@@ -0,0 +1,37 @@
+import arrayifyStream from 'arrayify-stream';
+import { BasicRepresentation } from '../../ldp/representation/BasicRepresentation';
+import type { Representation } from '../../ldp/representation/Representation';
+import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata';
+import { INTERNAL_ERROR, INTERNAL_QUADS } from '../../util/ContentTypes';
+import { InternalServerError } from '../../util/errors/InternalServerError';
+import { DC, SOLID_ERROR } from '../../util/Vocabularies';
+import type { RepresentationConverterArgs } from './RepresentationConverter';
+import { TypedRepresentationConverter } from './TypedRepresentationConverter';
+
+/**
+ * Converts an error object into quads by creating a triple for each of name/message/stack.
+ */
+export class ErrorToQuadConverter extends TypedRepresentationConverter {
+  public constructor() {
+    super(INTERNAL_ERROR, INTERNAL_QUADS);
+  }
+
+  public async handle({ identifier, representation }: RepresentationConverterArgs): Promise<Representation> {
+    const errors = await arrayifyStream(representation.data);
+    if (errors.length !== 1) {
+      throw new InternalServerError('Only single errors are supported.');
+    }
+    const error = errors[0] as Error;
+
+    // A metadata object makes it easier to add triples due to the utility functions
+    const data = new RepresentationMetadata(identifier);
+    data.add(DC.terms.title, error.name);
+    data.add(DC.terms.description, error.message);
+    if (error.stack) {
+      data.add(SOLID_ERROR.terms.stack, error.stack);
+    }
+
+    // Update the content-type to quads
+    return new BasicRepresentation(data.quads(), representation.metadata, INTERNAL_QUADS, false);
+  }
+}
diff --git a/src/util/ContentTypes.ts b/src/util/ContentTypes.ts
index 82a9f4269..40cd83776 100644
--- a/src/util/ContentTypes.ts
+++ b/src/util/ContentTypes.ts
@@ -8,3 +8,4 @@ export const APPLICATION_X_WWW_FORM_URLENCODED = 'application/x-www-form-urlenco
 // Internal content types (not exposed over HTTP)
 export const INTERNAL_ALL = 'internal/*';
 export const INTERNAL_QUADS = 'internal/quads';
+export const INTERNAL_ERROR = 'internal/error';
diff --git a/src/util/Vocabularies.ts b/src/util/Vocabularies.ts
index a8ab1b99c..b4fb7c048 100644
--- a/src/util/Vocabularies.ts
+++ b/src/util/Vocabularies.ts
@@ -75,16 +75,17 @@ export const AUTH = createUriAndTermNamespace('urn:solid:auth:',
 );
 
 export const DC = createUriAndTermNamespace('http://purl.org/dc/terms/',
+  'description',
   'modified',
+  'title',
 );
 
 export const FOAF = createUriAndTermNamespace('http://xmlns.com/foaf/0.1/',
   'Agent',
 );
 
-export const HTTP = createUriAndTermNamespace('urn:solid:http:',
-  'location',
-  'slug',
+export const HTTP = createUriAndTermNamespace('http://www.w3.org/2011/http#',
+  'statusCodeNumber',
 );
 
 export const LDP = createUriAndTermNamespace('http://www.w3.org/ns/ldp#',
@@ -112,6 +113,21 @@ export const RDF = createUriAndTermNamespace('http://www.w3.org/1999/02/22-rdf-s
   'type',
 );
 
+export const SOLID = createUriAndTermNamespace('http://www.w3.org/ns/solid/terms#',
+  'oidcIssuer',
+  'oidcIssuerRegistrationToken',
+  'oidcRegistration',
+);
+
+export const SOLID_ERROR = createUriAndTermNamespace('urn:npm:solid:community-server:error:',
+  'stack',
+);
+
+export const SOLID_HTTP = createUriAndTermNamespace('urn:npm:solid:community-server:http:',
+  'location',
+  'slug',
+);
+
 export const VANN = createUriAndTermNamespace('http://purl.org/vocab/vann/',
   'preferredNamespacePrefix',
 );
@@ -121,12 +137,6 @@ export const XSD = createUriAndTermNamespace('http://www.w3.org/2001/XMLSchema#'
   'integer',
 );
 
-export const SOLID = createUriAndTermNamespace('http://www.w3.org/ns/solid/terms#',
-  'oidcIssuer',
-  'oidcIssuerRegistrationToken',
-  'oidcRegistration',
-);
-
 // Alias for commonly used types
 export const CONTENT_TYPE = MA.format;
 export const CONTENT_TYPE_TERM = MA.terms.format;
diff --git a/src/util/errors/ErrorUtil.ts b/src/util/errors/ErrorUtil.ts
index 0c4d5c444..3d2dbce8b 100644
--- a/src/util/errors/ErrorUtil.ts
+++ b/src/util/errors/ErrorUtil.ts
@@ -1,4 +1,5 @@
 import { types } from 'util';
+import { HttpError } from './HttpError';
 
 /**
  * Checks if the input is an {@link Error}.
@@ -6,3 +7,20 @@ import { types } from 'util';
 export function isNativeError(error: any): error is Error {
   return types.isNativeError(error);
 }
+
+/**
+ * Asserts that the input is a native error.
+ * If not the input will be re-thrown.
+ */
+export function assertNativeError(error: any): asserts error is Error {
+  if (!isNativeError(error)) {
+    throw error;
+  }
+}
+
+/**
+ * Returns the HTTP status code corresponding to the error.
+ */
+export function getStatusCode(error: Error): number {
+  return HttpError.isInstance(error) ? error.statusCode : 500;
+}


Test patch:
diff --git a/test/integration/DynamicPods.test.ts b/test/integration/DynamicPods.test.ts
index 09defde89..7ed55ed8f 100644
--- a/test/integration/DynamicPods.test.ts
+++ b/test/integration/DynamicPods.test.ts
@@ -38,6 +38,7 @@ describe.each(configs)('A dynamic pod server with template config %s', (template
       'urn:solid-server:default:variable:baseUrl': baseUrl,
       'urn:solid-server:default:variable:rootFilePath': rootFilePath,
       'urn:solid-server:default:variable:podConfigJson': podConfigJson,
+      'urn:solid-server:default:variable:showStackTrace': true,
       'urn:solid-server:default:variable:idpTemplateFolder': joinFilePath(__dirname, '../../templates/idp'),
     };
 
diff --git a/test/integration/Identity.test.ts b/test/integration/Identity.test.ts
index fb795a161..49d48ca44 100644
--- a/test/integration/Identity.test.ts
+++ b/test/integration/Identity.test.ts
@@ -63,6 +63,7 @@ describe('A Solid server with IDP', (): void => {
       getTestConfigPath('server-memory.json'),
       {
         'urn:solid-server:default:variable:baseUrl': baseUrl,
+        'urn:solid-server:default:variable:showStackTrace': true,
         'urn:solid-server:default:variable:idpTemplateFolder': joinFilePath(__dirname, '../../templates/idp'),
       },
     ) as Record<string, any>;
diff --git a/test/integration/LdpHandlerWithAuth.test.ts b/test/integration/LdpHandlerWithAuth.test.ts
index a44692a98..05c4eb1d1 100644
--- a/test/integration/LdpHandlerWithAuth.test.ts
+++ b/test/integration/LdpHandlerWithAuth.test.ts
@@ -40,6 +40,7 @@ describe.each(stores)('An LDP handler with auth using %s', (name, { storeConfig,
   beforeAll(async(): Promise<void> => {
     const variables: Record<string, any> = {
       'urn:solid-server:default:variable:baseUrl': baseUrl,
+      'urn:solid-server:default:variable:showStackTrace': true,
       'urn:solid-server:default:variable:rootFilePath': rootFilePath,
     };
 
diff --git a/test/integration/LdpHandlerWithoutAuth.test.ts b/test/integration/LdpHandlerWithoutAuth.test.ts
index 009c7c5f0..e2249434f 100644
--- a/test/integration/LdpHandlerWithoutAuth.test.ts
+++ b/test/integration/LdpHandlerWithoutAuth.test.ts
@@ -34,6 +34,7 @@ describe.each(stores)('An LDP handler allowing all requests %s', (name, { storeC
   beforeAll(async(): Promise<void> => {
     const variables: Record<string, any> = {
       'urn:solid-server:default:variable:baseUrl': baseUrl,
+      'urn:solid-server:default:variable:showStackTrace': true,
       'urn:solid-server:default:variable:rootFilePath': rootFilePath,
     };
 
diff --git a/test/integration/Middleware.test.ts b/test/integration/Middleware.test.ts
index 9410cc7f0..9d7feb393 100644
--- a/test/integration/Middleware.test.ts
+++ b/test/integration/Middleware.test.ts
@@ -25,6 +25,7 @@ describe('An http server with middleware', (): void => {
       {
         'urn:solid-server:default:LdpHandler': new SimpleHttpHandler(),
         'urn:solid-server:default:variable:baseUrl': 'https://example.pod/',
+        'urn:solid-server:default:variable:showStackTrace': true,
       },
     ) as BaseHttpServerFactory;
     server = factory.startServer(port);
diff --git a/test/integration/RedisResourceLockerIntegration.test.ts b/test/integration/RedisResourceLockerIntegration.test.ts
index 7c5c71584..d31a714b7 100644
--- a/test/integration/RedisResourceLockerIntegration.test.ts
+++ b/test/integration/RedisResourceLockerIntegration.test.ts
@@ -21,6 +21,7 @@ describeIf('docker', 'A server with a RedisResourceLocker as ResourceLocker', ()
       getTestConfigPath('run-with-redlock.json'),
       {
         'urn:solid-server:default:variable:baseUrl': baseUrl,
+        'urn:solid-server:default:variable:showStackTrace': true,
       },
     ) as Record<string, any>;
     ({ factory, locker } = instances);
diff --git a/test/integration/ServerFetch.test.ts b/test/integration/ServerFetch.test.ts
index c91b87e1f..6c6df074b 100644
--- a/test/integration/ServerFetch.test.ts
+++ b/test/integration/ServerFetch.test.ts
@@ -22,6 +22,7 @@ describe('A Solid server', (): void => {
       getTestConfigPath('server-memory.json'),
       {
         'urn:solid-server:default:variable:baseUrl': baseUrl,
+        'urn:solid-server:default:variable:showStackTrace': true,
         'urn:solid-server:default:variable:idpTemplateFolder': '',
       },
     ) as Record<string, any>;
diff --git a/test/integration/SparqlStorage.test.ts b/test/integration/SparqlStorage.test.ts
index 490433b98..fdc3047a6 100644
--- a/test/integration/SparqlStorage.test.ts
+++ b/test/integration/SparqlStorage.test.ts
@@ -18,6 +18,7 @@ describeIf('docker', 'A server with a SPARQL endpoint as storage', (): void => {
   beforeAll(async(): Promise<void> => {
     const variables: Record<string, any> = {
       'urn:solid-server:default:variable:baseUrl': baseUrl,
+      'urn:solid-server:default:variable:showStackTrace': true,
       'urn:solid-server:default:variable:sparqlEndpoint': 'http://localhost:4000/sparql',
     };
 
diff --git a/test/integration/Subdomains.test.ts b/test/integration/Subdomains.test.ts
index c1b4b2d1e..4283a9a57 100644
--- a/test/integration/Subdomains.test.ts
+++ b/test/integration/Subdomains.test.ts
@@ -37,6 +37,7 @@ describe.each(stores)('A subdomain server with %s', (name, { storeConfig, teardo
     const variables: Record<string, any> = {
       'urn:solid-server:default:variable:baseUrl': baseUrl,
       'urn:solid-server:default:variable:rootFilePath': rootFilePath,
+      'urn:solid-server:default:variable:showStackTrace': true,
       'urn:solid-server:default:variable:idpTemplateFolder': joinFilePath(__dirname, '../../templates/idp'),
     };
 
diff --git a/test/integration/WebSocketsProtocol.test.ts b/test/integration/WebSocketsProtocol.test.ts
index 17cdeefdd..343bf23ea 100644
--- a/test/integration/WebSocketsProtocol.test.ts
+++ b/test/integration/WebSocketsProtocol.test.ts
@@ -18,6 +18,7 @@ describe('A server with the Solid WebSockets API behind a proxy', (): void => {
       getTestConfigPath('server-without-auth.json'),
       {
         'urn:solid-server:default:variable:baseUrl': 'https://example.pod/',
+        'urn:solid-server:default:variable:showStackTrace': true,
       },
     ) as HttpServerFactory;
     server = factory.startServer(port);
diff --git a/test/unit/identity/IdentityProviderFactory.test.ts b/test/unit/identity/IdentityProviderFactory.test.ts
index 626680db7..9f1983c54 100644
--- a/test/unit/identity/IdentityProviderFactory.test.ts
+++ b/test/unit/identity/IdentityProviderFactory.test.ts
@@ -2,7 +2,9 @@ import type { interactionPolicy, Configuration, KoaContextWithOIDC } from 'oidc-
 import type { ConfigurationFactory } from '../../../src/identity/configuration/ConfigurationFactory';
 import { IdentityProviderFactory } from '../../../src/identity/IdentityProviderFactory';
 import type { InteractionPolicy } from '../../../src/identity/interaction/InteractionPolicy';
+import type { ErrorHandler } from '../../../src/ldp/http/ErrorHandler';
 import type { ResponseWriter } from '../../../src/ldp/http/ResponseWriter';
+import type { HttpResponse } from '../../../src/server/HttpResponse';
 
 jest.mock('oidc-provider', (): any => ({
   // eslint-disable-next-line @typescript-eslint/naming-convention
@@ -17,7 +19,8 @@ describe('An IdentityProviderFactory', (): void => {
   };
   const webId = 'http://alice.test.com/card#me';
   let configuration: any;
-  let errorResponseWriter: ResponseWriter;
+  let errorHandler: ErrorHandler;
+  let responseWriter: ResponseWriter;
   let factory: IdentityProviderFactory;
 
   beforeEach(async(): Promise<void> => {
@@ -26,11 +29,13 @@ describe('An IdentityProviderFactory', (): void => {
       createConfiguration: async(): Promise<any> => configuration,
     };
 
-    errorResponseWriter = {
-      handleSafe: jest.fn(),
+    errorHandler = {
+      handleSafe: jest.fn().mockResolvedValue({ statusCode: 500 }),
     } as any;
 
-    factory = new IdentityProviderFactory(issuer, configurationFactory, errorResponseWriter);
+    responseWriter = { handleSafe: jest.fn() } as any;
+
+    factory = new IdentityProviderFactory(issuer, configurationFactory, errorHandler, responseWriter);
   });
 
   it('has fixed default values.', async(): Promise<void> => {
@@ -65,9 +70,14 @@ describe('An IdentityProviderFactory', (): void => {
       aud: 'solid',
     });
 
-    await expect(result.config.renderError({ res: 'response!' }, null, 'error!')).resolves.toBeUndefined();
-    expect(errorResponseWriter.handleSafe).toHaveBeenCalledTimes(1);
-    expect(errorResponseWriter.handleSafe).toHaveBeenLastCalledWith({ response: 'response!', result: 'error!' });
+    // Test the renderError function
+    const response: HttpResponse = { } as any;
+    await expect(result.config.renderError({ res: response }, null, 'error!')).resolves.toBeUndefined();
+    expect(errorHandler.handleSafe).toHaveBeenCalledTimes(1);
+    expect(errorHandler.handleSafe)
+      .toHaveBeenLastCalledWith({ error: 'error!', preferences: { type: { 'text/plain': 1 }}});
+    expect(responseWriter.handleSafe).toHaveBeenCalledTimes(1);
+    expect(responseWriter.handleSafe).toHaveBeenLastCalledWith({ response, result: { statusCode: 500 }});
   });
 
   it('overwrites fields from the factory config.', async(): Promise<void> => {
diff --git a/test/unit/identity/IdentityProviderHttpHandler.test.ts b/test/unit/identity/IdentityProviderHttpHandler.test.ts
index 5baeedcad..368fc00a5 100644
--- a/test/unit/identity/IdentityProviderHttpHandler.test.ts
+++ b/test/unit/identity/IdentityProviderHttpHandler.test.ts
@@ -3,6 +3,8 @@ import type { IdentityProviderFactory } from '../../../src/identity/IdentityProv
 import { IdentityProviderHttpHandler } from '../../../src/identity/IdentityProviderHttpHandler';
 import type { InteractionHttpHandler } from '../../../src/identity/interaction/InteractionHttpHandler';
 import type { InteractionPolicy } from '../../../src/identity/interaction/InteractionPolicy';
+import type { ErrorHandler } from '../../../src/ldp/http/ErrorHandler';
+import type { ResponseDescription } from '../../../src/ldp/http/response/ResponseDescription';
 import type { ResponseWriter } from '../../../src/ldp/http/ResponseWriter';
 import type { HttpRequest } from '../../../src/server/HttpRequest';
 import type { HttpResponse } from '../../../src/server/HttpResponse';
@@ -16,7 +18,8 @@ describe('An IdentityProviderHttpHandler', (): void => {
     url: (ctx: KoaContextWithOIDC): string => `/idp/interaction/${ctx.oidc.uid}`,
   };
   let interactionHttpHandler: InteractionHttpHandler;
-  let errorResponseWriter: ResponseWriter;
+  let errorHandler: ErrorHandler;
+  let responseWriter: ResponseWriter;
   let provider: Provider;
   let handler: IdentityProviderHttpHandler;
 
@@ -34,15 +37,16 @@ describe('An IdentityProviderHttpHandler', (): void => {
       handle: jest.fn(),
     } as any;
 
-    errorResponseWriter = {
-      handleSafe: jest.fn(),
-    } as any;
+    errorHandler = { handleSafe: jest.fn() } as any;
+
+    responseWriter = { handleSafe: jest.fn() } as any;
 
     handler = new IdentityProviderHttpHandler(
       providerFactory,
       idpPolicy,
       interactionHttpHandler,
-      errorResponseWriter,
+      errorHandler,
+      responseWriter,
     );
   });
 
@@ -52,7 +56,7 @@ describe('An IdentityProviderHttpHandler', (): void => {
     expect(provider.callback).toHaveBeenCalledTimes(1);
     expect(provider.callback).toHaveBeenLastCalledWith(request, response);
     expect(interactionHttpHandler.handle).toHaveBeenCalledTimes(0);
-    expect(errorResponseWriter.handleSafe).toHaveBeenCalledTimes(0);
+    expect(responseWriter.handleSafe).toHaveBeenCalledTimes(0);
   });
 
   it('calls the interaction handler if it can handle the input.', async(): Promise<void> => {
@@ -60,18 +64,22 @@ describe('An IdentityProviderHttpHandler', (): void => {
     expect(provider.callback).toHaveBeenCalledTimes(0);
     expect(interactionHttpHandler.handle).toHaveBeenCalledTimes(1);
     expect(interactionHttpHandler.handle).toHaveBeenLastCalledWith({ request, response, provider });
-    expect(errorResponseWriter.handleSafe).toHaveBeenCalledTimes(0);
+    expect(responseWriter.handleSafe).toHaveBeenCalledTimes(0);
   });
 
-  it('calls the errorResponseWriter if there was an issue with the interaction handler.', async(): Promise<void> => {
+  it('returns an error response if there was an issue with the interaction handler.', async(): Promise<void> => {
     const error = new Error('error!');
+    const errorResponse: ResponseDescription = { statusCode: 500 };
     (interactionHttpHandler.handle as jest.Mock).mockRejectedValueOnce(error);
+    (errorHandler.handleSafe as jest.Mock).mockResolvedValueOnce(errorResponse);
     await expect(handler.handle({ request, response })).resolves.toBeUndefined();
     expect(provider.callback).toHaveBeenCalledTimes(0);
     expect(interactionHttpHandler.handle).toHaveBeenCalledTimes(1);
     expect(interactionHttpHandler.handle).toHaveBeenLastCalledWith({ request, response, provider });
-    expect(errorResponseWriter.handleSafe).toHaveBeenCalledTimes(1);
-    expect(errorResponseWriter.handleSafe).toHaveBeenLastCalledWith({ response, result: error });
+    expect(errorHandler.handleSafe).toHaveBeenCalledTimes(1);
+    expect(errorHandler.handleSafe).toHaveBeenLastCalledWith({ error, preferences: { type: { 'text/plain': 1 }}});
+    expect(responseWriter.handleSafe).toHaveBeenCalledTimes(1);
+    expect(responseWriter.handleSafe).toHaveBeenLastCalledWith({ response, result: errorResponse });
   });
 
   it('re-throws the error if it is not a native Error.', async(): Promise<void> => {
diff --git a/test/unit/init/AppRunner.test.ts b/test/unit/init/AppRunner.test.ts
index 7b5b1af50..af8288dc2 100644
--- a/test/unit/init/AppRunner.test.ts
+++ b/test/unit/init/AppRunner.test.ts
@@ -44,6 +44,7 @@ describe('AppRunner', (): void => {
           port: 3000,
           loggingLevel: 'info',
           rootFilePath: '/var/cwd/',
+          showStackTrace: false,
           podConfigJson: '/var/cwd/pod-config.json',
         },
       );
@@ -67,6 +68,7 @@ describe('AppRunner', (): void => {
             'urn:solid-server:default:variable:rootFilePath': '/var/cwd/',
             'urn:solid-server:default:variable:sparqlEndpoint': undefined,
             'urn:solid-server:default:variable:loggingLevel': 'info',
+            'urn:solid-server:default:variable:showStackTrace': false,
             'urn:solid-server:default:variable:podConfigJson': '/var/cwd/pod-config.json',
             'urn:solid-server:default:variable:idpTemplateFolder': joinFilePath(__dirname, '../../../templates/idp'),
           },
@@ -107,6 +109,7 @@ describe('AppRunner', (): void => {
             'urn:solid-server:default:variable:rootFilePath': '/var/cwd/',
             'urn:solid-server:default:variable:sparqlEndpoint': undefined,
             'urn:solid-server:default:variable:loggingLevel': 'info',
+            'urn:solid-server:default:variable:showStackTrace': false,
             'urn:solid-server:default:variable:podConfigJson': '/var/cwd/pod-config.json',
             'urn:solid-server:default:variable:idpTemplateFolder': joinFilePath(__dirname, '../../../templates/idp'),
           },
@@ -127,6 +130,7 @@ describe('AppRunner', (): void => {
           '-m', 'module/path',
           '-p', '4000',
           '-s', 'http://localhost:5000/sparql',
+          '-t',
           '--podConfigJson', '/different-path.json',
           '--idpTemplateFolder', 'templates/idp',
         ],
@@ -154,6 +158,7 @@ describe('AppRunner', (): void => {
             'urn:solid-server:default:variable:port': 4000,
             'urn:solid-server:default:variable:rootFilePath': '/root',
             'urn:solid-server:default:variable:sparqlEndpoint': 'http://localhost:5000/sparql',
+            'urn:solid-server:default:variable:showStackTrace': true,
             'urn:solid-server:default:variable:podConfigJson': '/different-path.json',
             'urn:solid-server:default:variable:idpTemplateFolder': '/var/cwd/templates/idp',
           },
@@ -172,6 +177,7 @@ describe('AppRunner', (): void => {
           '--port', '4000',
           '--rootFilePath', 'root',
           '--sparqlEndpoint', 'http://localhost:5000/sparql',
+          '--showStackTrace',
           '--podConfigJson', '/different-path.json',
           '--idpTemplateFolder', 'templates/idp',
         ],
@@ -199,6 +205,7 @@ describe('AppRunner', (): void => {
             'urn:solid-server:default:variable:port': 4000,
             'urn:solid-server:default:variable:rootFilePath': '/var/cwd/root',
             'urn:solid-server:default:variable:sparqlEndpoint': 'http://localhost:5000/sparql',
+            'urn:solid-server:default:variable:showStackTrace': true,
             'urn:solid-server:default:variable:podConfigJson': '/different-path.json',
             'urn:solid-server:default:variable:idpTemplateFolder': '/var/cwd/templates/idp',
           },
@@ -217,6 +224,7 @@ describe('AppRunner', (): void => {
         '-m', 'module/path',
         '-p', '4000',
         '-s', 'http://localhost:5000/sparql',
+        '-t',
         '--podConfigJson', '/different-path.json',
         '--idpTemplateFolder', 'templates/idp',
       ];
@@ -245,6 +253,7 @@ describe('AppRunner', (): void => {
             'urn:solid-server:default:variable:port': 4000,
             'urn:solid-server:default:variable:rootFilePath': '/root',
             'urn:solid-server:default:variable:sparqlEndpoint': 'http://localhost:5000/sparql',
+            'urn:solid-server:default:variable:showStackTrace': true,
             'urn:solid-server:default:variable:podConfigJson': '/different-path.json',
             'urn:solid-server:default:variable:idpTemplateFolder': '/var/cwd/templates/idp',
           },
@@ -293,9 +302,7 @@ describe('AppRunner', (): void => {
 
     it('exits when unknown options are passed to the main executable.', async(): Promise<void> => {
       new AppRunner().runCli({
-        argv: [
-          'node', 'script', '--foo',
-        ],
+        argv: [ 'node', 'script', '--foo' ],
       });
 
       // Wait until initializer has been called, because we can't await AppRunner.run.
@@ -303,16 +310,14 @@ describe('AppRunner', (): void => {
         setImmediate(resolve);
       });
 
-      expect(error).toHaveBeenCalledWith('Unknown option: "foo"');
+      expect(error).toHaveBeenCalledWith('Unknown argument: foo');
       expect(exit).toHaveBeenCalledTimes(1);
       expect(exit).toHaveBeenCalledWith(1);
     });
 
     it('exits when no value is passed to the main executable for an argument.', async(): Promise<void> => {
       new AppRunner().runCli({
-        argv: [
-          'node', 'script', '-s',
-        ],
+        argv: [ 'node', 'script', '-s' ],
       });
 
       // Wait until initializer has been called, because we can't await AppRunner.run.
@@ -320,16 +325,14 @@ describe('AppRunner', (): void => {
         setImmediate(resolve);
       });
 
-      expect(error).toHaveBeenCalledWith('Missing value for argument "s"');
+      expect(error).toHaveBeenCalledWith('Not enough arguments following: s');
       expect(exit).toHaveBeenCalledTimes(1);
       expect(exit).toHaveBeenCalledWith(1);
     });
 
     it('exits when unknown parameters are passed to the main executable.', async(): Promise<void> => {
       new AppRunner().runCli({
-        argv: [
-          'node', 'script', 'foo', 'bar', 'foo.txt', 'bar.txt',
-        ],
+        argv: [ 'node', 'script', 'foo', 'bar', 'foo.txt', 'bar.txt' ],
       });
 
       // Wait until initializer has been called, because we can't await AppRunner.run.
@@ -337,17 +340,14 @@ describe('AppRunner', (): void => {
         setImmediate(resolve);
       });
 
-      // There seems to be an issue with yargs where the first and last `"` are missing.
-      expect(error).toHaveBeenCalledWith('Unsupported arguments: foo", "bar", "foo.txt", "bar.txt');
+      expect(error).toHaveBeenCalledWith('Unsupported positional arguments: "foo", "bar", "foo.txt", "bar.txt"');
       expect(exit).toHaveBeenCalledTimes(1);
       expect(exit).toHaveBeenCalledWith(1);
     });
 
     it('exits when multiple values for a parameter are passed.', async(): Promise<void> => {
       new AppRunner().runCli({
-        argv: [
-          'node', 'script', '-ll',
-        ],
+        argv: [ 'node', 'script', '-l', 'info', '-l', 'debug' ],
       });
 
       // Wait until initializer has been called, because we can't await AppRunner.run.
@@ -355,7 +355,7 @@ describe('AppRunner', (): void => {
         setImmediate(resolve);
       });
 
-      expect(error).toHaveBeenCalledWith('Multiple values were provided for: "l", [info,info]');
+      expect(error).toHaveBeenCalledWith('Multiple values were provided for: "l": "info", "debug"');
       expect(exit).toHaveBeenCalledTimes(1);
       expect(exit).toHaveBeenCalledWith(1);
     });
diff --git a/test/unit/ldp/AuthenticatedLdpHandler.test.ts b/test/unit/ldp/AuthenticatedLdpHandler.test.ts
index c18e9768b..05b63782e 100644
--- a/test/unit/ldp/AuthenticatedLdpHandler.test.ts
+++ b/test/unit/ldp/AuthenticatedLdpHandler.test.ts
@@ -1,80 +1,117 @@
-import type { CredentialsExtractor } from '../../../src/authentication/CredentialsExtractor';
-import type { Authorizer } from '../../../src/authorization/Authorizer';
+import type { Credentials } from '../../../src/authentication/Credentials';
+import type { Authorization } from '../../../src/authorization/Authorization';
 import type { AuthenticatedLdpHandlerArgs } from '../../../src/ldp/AuthenticatedLdpHandler';
 import { AuthenticatedLdpHandler } from '../../../src/ldp/AuthenticatedLdpHandler';
-import type { RequestParser } from '../../../src/ldp/http/RequestParser';
-import type { ResponseWriter } from '../../../src/ldp/http/ResponseWriter';
+import { ResetResponseDescription } from '../../../src/ldp/http/response/ResetResponseDescription';
+import type { ResponseDescription } from '../../../src/ldp/http/response/ResponseDescription';
 import type { Operation } from '../../../src/ldp/operations/Operation';
-import type { OperationHandler } from '../../../src/ldp/operations/OperationHandler';
-import type { PermissionsExtractor } from '../../../src/ldp/permissions/PermissionsExtractor';
+import type { PermissionSet } from '../../../src/ldp/permissions/PermissionSet';
+import type { RepresentationPreferences } from '../../../src/ldp/representation/RepresentationPreferences';
+import * as LogUtil from '../../../src/logging/LogUtil';
 import type { HttpRequest } from '../../../src/server/HttpRequest';
 import type { HttpResponse } from '../../../src/server/HttpResponse';
-import { StaticAsyncHandler } from '../../util/StaticAsyncHandler';
 
 describe('An AuthenticatedLdpHandler', (): void => {
+  const request: HttpRequest = {} as any;
+  const response: HttpResponse = {} as any;
+  const preferences: RepresentationPreferences = { type: { 'text/turtle': 0.9 }};
+  let operation: Operation;
+  const credentials: Credentials = {};
+  const permissions: PermissionSet = { read: true, write: false, append: false, control: false };
+  const authorization: Authorization = { addMetadata: jest.fn() };
+  const result: ResponseDescription = new ResetResponseDescription();
+  const errorResult: ResponseDescription = { statusCode: 500 };
   let args: AuthenticatedLdpHandlerArgs;
-  let responseFn: jest.Mock<Promise<void>, [any]>;
+  let handler: AuthenticatedLdpHandler;
 
   beforeEach(async(): Promise<void> => {
-    const requestParser: RequestParser = new StaticAsyncHandler(true, ({ target: 'target' }) as any);
-    const credentialsExtractor: CredentialsExtractor = new StaticAsyncHandler(true, 'credentials' as any);
-    const permissionsExtractor: PermissionsExtractor = new StaticAsyncHandler(true, 'permissions' as any);
-    const authorizer: Authorizer = new StaticAsyncHandler(true, 'authorizer' as any);
-    const operationHandler: OperationHandler = new StaticAsyncHandler(true, 'operation' as any);
-    const responseWriter: ResponseWriter = new StaticAsyncHandler(true, 'response' as any);
-
-    responseFn = jest.fn(async(input: any): Promise<void> => {
-      if (!input) {
-        throw new Error('error');
-      }
-    });
-    responseWriter.canHandle = responseFn;
-
-    args = { requestParser, credentialsExtractor, permissionsExtractor, authorizer, operationHandler, responseWriter };
+    operation = { target: { path: 'identifier' }, method: 'GET', preferences };
+    args = {
+      requestParser: {
+        canHandle: jest.fn(),
+        handleSafe: jest.fn().mockResolvedValue(operation),
+      } as any,
+      credentialsExtractor: { handleSafe: jest.fn().mockResolvedValue(credentials) } as any,
+      permissionsExtractor: { handleSafe: jest.fn().mockResolvedValue(permissions) } as any,
+      authorizer: { handleSafe: jest.fn().mockResolvedValue(authorization) } as any,
+      operationHandler: { handleSafe: jest.fn().mockResolvedValue(result) } as any,
+      errorHandler: { handleSafe: jest.fn().mockResolvedValue(errorResult) } as any,
+      responseWriter: { handleSafe: jest.fn() } as any,
+    };
+    handler = new AuthenticatedLdpHandler(args);
   });
 
   it('can be created.', async(): Promise<void> => {
     expect(new AuthenticatedLdpHandler(args)).toBeInstanceOf(AuthenticatedLdpHandler);
   });
 
-  it('can check if it handles input.', async(): Promise<void> => {
-    const handler = new AuthenticatedLdpHandler(args);
+  it('can not handle the input if the RequestParser rejects it.', async(): Promise<void> => {
+    (args.requestParser.canHandle as jest.Mock).mockRejectedValueOnce(new Error('bad data!'));
+    await expect(handler.canHandle({ request, response })).rejects.toThrow('bad data!');
+    expect(args.requestParser.canHandle).toHaveBeenLastCalledWith(request);
+  });
 
-    await expect(handler.canHandle(
-      { request: {} as HttpRequest, response: {} as HttpResponse },
-    )).resolves.toBeUndefined();
+  it('can handle the input if the RequestParser can handle it.', async(): Promise<void> => {
+    await expect(handler.canHandle({ request, response })).resolves.toBeUndefined();
+    expect(args.requestParser.canHandle).toHaveBeenCalledTimes(1);
+    expect(args.requestParser.canHandle).toHaveBeenLastCalledWith(request);
   });
 
   it('can handle input.', async(): Promise<void> => {
-    const handler = new AuthenticatedLdpHandler(args);
-
-    await expect(handler.handle({ request: 'request' as any, response: 'response' as any })).resolves.toBeUndefined();
-    expect(responseFn).toHaveBeenCalledTimes(1);
-    expect(responseFn).toHaveBeenLastCalledWith({ response: 'response', result: 'operation' as any });
+    await expect(handler.handle({ request, response })).resolves.toBeUndefined();
+    expect(args.requestParser.handleSafe).toHaveBeenCalledTimes(1);
+    expect(args.requestParser.handleSafe).toHaveBeenLastCalledWith(request);
+    expect(args.credentialsExtractor.handleSafe).toHaveBeenCalledTimes(1);
+    expect(args.credentialsExtractor.handleSafe).toHaveBeenLastCalledWith(request);
+    expect(args.permissionsExtractor.handleSafe).toHaveBeenCalledTimes(1);
+    expect(args.permissionsExtractor.handleSafe).toHaveBeenLastCalledWith(operation);
+    expect(args.authorizer.handleSafe).toHaveBeenCalledTimes(1);
+    expect(args.authorizer.handleSafe)
+      .toHaveBeenLastCalledWith({ credentials, identifier: { path: 'identifier' }, permissions });
+    expect(operation.authorization).toBe(authorization);
+    expect(args.operationHandler.handleSafe).toHaveBeenCalledTimes(1);
+    expect(args.operationHandler.handleSafe).toHaveBeenLastCalledWith(operation);
+    expect(args.errorHandler.handleSafe).toHaveBeenCalledTimes(0);
+    expect(args.responseWriter.handleSafe).toHaveBeenCalledTimes(1);
+    expect(args.responseWriter.handleSafe).toHaveBeenLastCalledWith({ response, result });
   });
 
-  it('sends an error to the output if a handler does not support the input.', async(): Promise<void> => {
-    args.requestParser = new StaticAsyncHandler(false, {} as Operation);
-    const handler = new AuthenticatedLdpHandler(args);
+  it('sets preferences to text/plain in case of an error during request parsing.', async(): Promise<void> => {
+    const error = new Error('bad request!');
+    (args.requestParser.handleSafe as jest.Mock).mockRejectedValueOnce(new Error('bad request!'));
 
-    await expect(handler.handle({ request: 'request' as any, response: {} as HttpResponse })).resolves.toBeUndefined();
-    expect(responseFn).toHaveBeenCalledTimes(1);
-    expect(responseFn.mock.calls[0][0].result).toBeInstanceOf(Error);
+    await expect(handler.handle({ request, response })).resolves.toBeUndefined();
+    expect(args.requestParser.handleSafe).toHaveBeenCalledTimes(1);
+    expect(args.credentialsExtractor.handleSafe).toHaveBeenCalledTimes(0);
+    expect(args.errorHandler.handleSafe).toHaveBeenCalledTimes(1);
+    expect(args.errorHandler.handleSafe).toHaveBeenLastCalledWith({ error, preferences: { type: { 'text/plain': 1 }}});
+    expect(args.responseWriter.handleSafe).toHaveBeenCalledTimes(1);
+    expect(args.responseWriter.handleSafe).toHaveBeenLastCalledWith({ response, result: errorResult });
   });
 
-  it('errors if the response writer does not support the result.', async(): Promise< void> => {
-    args.responseWriter = new StaticAsyncHandler(false, undefined);
-    const handler = new AuthenticatedLdpHandler(args);
+  it('sets preferences to the request preferences if they were parsed before the error.', async(): Promise<void> => {
+    const error = new Error('bad request!');
+    (args.credentialsExtractor.handleSafe as jest.Mock).mockRejectedValueOnce(new Error('bad request!'));
 
-    await expect(handler.handle({ request: 'request' as any, response: {} as HttpResponse })).rejects.toThrow(Error);
+    await expect(handler.handle({ request, response })).resolves.toBeUndefined();
+    expect(args.requestParser.handleSafe).toHaveBeenCalledTimes(1);
+    expect(args.credentialsExtractor.handleSafe).toHaveBeenCalledTimes(1);
+    expect(args.authorizer.handleSafe).toHaveBeenCalledTimes(0);
+    expect(args.errorHandler.handleSafe).toHaveBeenCalledTimes(1);
+    expect(args.errorHandler.handleSafe).toHaveBeenLastCalledWith({ error, preferences });
+    expect(args.responseWriter.handleSafe).toHaveBeenCalledTimes(1);
+    expect(args.responseWriter.handleSafe).toHaveBeenLastCalledWith({ response, result: errorResult });
   });
 
-  it('errors an invalid object was thrown by a handler.', async(): Promise< void> => {
-    args.authorizer.handle = async(): Promise<any> => {
-      throw 'apple';
-    };
-    const handler = new AuthenticatedLdpHandler(args);
+  it('logs an error if authorization failed.', async(): Promise<void> => {
+    const logger = { verbose: jest.fn() };
+    const mock = jest.spyOn(LogUtil, 'getLoggerFor');
+    mock.mockReturnValueOnce(logger as any);
+    handler = new AuthenticatedLdpHandler(args);
+    (args.authorizer.handleSafe as jest.Mock).mockRejectedValueOnce(new Error('bad auth!'));
+    await expect(handler.handle({ request, response })).resolves.toBeUndefined();
+    expect(logger.verbose).toHaveBeenLastCalledWith('Authorization failed: bad auth!');
 
-    await expect(handler.handle({ request: 'request' as any, response: {} as HttpResponse })).rejects.toEqual('apple');
+    mock.mockRestore();
   });
 });
diff --git a/test/unit/ldp/http/ConvertingErrorHandler.test.ts b/test/unit/ldp/http/ConvertingErrorHandler.test.ts
new file mode 100644
index 000000000..dd209244f
--- /dev/null
+++ b/test/unit/ldp/http/ConvertingErrorHandler.test.ts
@@ -0,0 +1,95 @@
+import 'jest-rdf';
+import arrayifyStream from 'arrayify-stream';
+import { DataFactory } from 'n3';
+import { ConvertingErrorHandler } from '../../../../src/ldp/http/ConvertingErrorHandler';
+import { BasicRepresentation } from '../../../../src/ldp/representation/BasicRepresentation';
+import type { Representation } from '../../../../src/ldp/representation/Representation';
+import type { RepresentationPreferences } from '../../../../src/ldp/representation/RepresentationPreferences';
+import type {
+  RepresentationConverter,
+  RepresentationConverterArgs,
+} from '../../../../src/storage/conversion/RepresentationConverter';
+import { NotFoundHttpError } from '../../../../src/util/errors/NotFoundHttpError';
+import { HTTP, XSD } from '../../../../src/util/Vocabularies';
+import literal = DataFactory.literal;
+
+const preferences: RepresentationPreferences = { type: { 'text/turtle': 1 }};
+
+async function expectValidArgs(args: RepresentationConverterArgs, stack?: string): Promise<void> {
+  expect(args.preferences).toBe(preferences);
+  expect(args.representation.metadata.get(HTTP.terms.statusCodeNumber))
+    .toEqualRdfTerm(literal(404, XSD.terms.integer));
+  expect(args.representation.metadata.contentType).toBe('internal/error');
+
+  // Error contents
+  const errorArray = await arrayifyStream(args.representation.data);
+  expect(errorArray).toHaveLength(1);
+  const resultError = errorArray[0];
+  expect(resultError).toMatchObject({ name: 'NotFoundHttpError', message: 'not here' });
+  expect(resultError.stack).toBe(stack);
+}
+
+describe('A ConvertingErrorHandler', (): void => {
+  // The error object can get modified by the handler
+  let error: Error;
+  let stack: string | undefined;
+  let converter: RepresentationConverter;
+  let handler: ConvertingErrorHandler;
+
+  beforeEach(async(): Promise<void> => {
+    error = new NotFoundHttpError('not here');
+    ({ stack } = error);
+    converter = {
+      canHandle: jest.fn(),
+      handle: jest.fn((): Representation => new BasicRepresentation('serialization', 'text/turtle', true)),
+      handleSafe: jest.fn((): Representation => new BasicRepresentation('serialization', 'text/turtle', true)),
+    } as any;
+
+    handler = new ConvertingErrorHandler(converter, true);
+  });
+
+  it('rejects input not supported by the converter.', async(): Promise<void> => {
+    (converter.canHandle as jest.Mock).mockRejectedValueOnce(new Error('rejected'));
+    await expect(handler.canHandle({ error, preferences })).rejects.toThrow('rejected');
+    expect(converter.canHandle).toHaveBeenCalledTimes(1);
+    const args = (converter.canHandle as jest.Mock).mock.calls[0][0] as RepresentationConverterArgs;
+    expect(args.preferences).toBe(preferences);
+    expect(args.representation.metadata.contentType).toBe('internal/error');
+  });
+
+  it('accepts input supported by the converter.', async(): Promise<void> => {
+    await expect(handler.canHandle({ error, preferences })).resolves.toBeUndefined();
+    expect(converter.canHandle).toHaveBeenCalledTimes(1);
+    const args = (converter.canHandle as jest.Mock).mock.calls[0][0] as RepresentationConverterArgs;
+    expect(args.preferences).toBe(preferences);
+    expect(args.representation.metadata.contentType).toBe('internal/error');
+  });
+
+  it('returns the converted error response.', async(): Promise<void> => {
+    const prom = handler.handle({ error, preferences });
+    await expect(prom).resolves.toMatchObject({ statusCode: 404 });
+    expect((await prom).metadata?.contentType).toBe('text/turtle');
+    expect(converter.handle).toHaveBeenCalledTimes(1);
+    const args = (converter.handle as jest.Mock).mock.calls[0][0] as RepresentationConverterArgs;
+    await expectValidArgs(args, stack);
+  });
+
+  it('uses the handleSafe function of the converter during its own handleSafe call.', async(): Promise<void> => {
+    const prom = handler.handleSafe({ error, preferences });
+    await expect(prom).resolves.toMatchObject({ statusCode: 404 });
+    expect((await prom).metadata?.contentType).toBe('text/turtle');
+    expect(converter.handleSafe).toHaveBeenCalledTimes(1);
+    const args = (converter.handleSafe as jest.Mock).mock.calls[0][0] as RepresentationConverterArgs;
+    await expectValidArgs(args, stack);
+  });
+
+  it('hides the stack trace if the option is disabled.', async(): Promise<void> => {
+    handler = new ConvertingErrorHandler(converter);
+    const prom = handler.handle({ error, preferences });
+    await expect(prom).resolves.toMatchObject({ statusCode: 404 });
+    expect((await prom).metadata?.contentType).toBe('text/turtle');
+    expect(converter.handle).toHaveBeenCalledTimes(1);
+    const args = (converter.handle as jest.Mock).mock.calls[0][0] as RepresentationConverterArgs;
+    await expectValidArgs(args);
+  });
+});
diff --git a/test/unit/ldp/http/ErrorResponseWriter.test.ts b/test/unit/ldp/http/ErrorResponseWriter.test.ts
deleted file mode 100644
index 1a7efe1d5..000000000
--- a/test/unit/ldp/http/ErrorResponseWriter.test.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import { EventEmitter } from 'events';
-import type { MockResponse } from 'node-mocks-http';
-import { createResponse } from 'node-mocks-http';
-import { ErrorResponseWriter } from '../../../../src/ldp/http/ErrorResponseWriter';
-import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
-
-describe('An ErrorResponseWriter', (): void => {
-  const writer = new ErrorResponseWriter();
-  let response: MockResponse<any>;
-
-  beforeEach(async(): Promise<void> => {
-    response = createResponse({ eventEmitter: EventEmitter });
-  });
-
-  it('requires the input to be an error.', async(): Promise<void> => {
-    await expect(writer.canHandle({ response, result: new Error('error') }))
-      .resolves.toBeUndefined();
-    await expect(writer.canHandle({ response, result: { statusCode: 200 }}))
-      .rejects.toThrow(NotImplementedHttpError);
-  });
-
-  it('responds with 500 if an error if there is an error.', async(): Promise<void> => {
-    await writer.handle({ response, result: new Error('error') });
-    expect(response._isEndCalled()).toBeTruthy();
-    expect(response._getStatusCode()).toBe(500);
-    expect(response._getData()).toMatch('Error: error');
-  });
-
-  it('responds with the given statuscode if there is an HttpError.', async(): Promise<void> => {
-    const error = new NotImplementedHttpError('error');
-    await writer.handle({ response, result: error });
-    expect(response._isEndCalled()).toBeTruthy();
-    expect(response._getStatusCode()).toBe(error.statusCode);
-    expect(response._getData()).toMatch('NotImplementedHttpError: error');
-  });
-
-  it('responds with the error name and message when no stack trace is lazily generated.', async(): Promise<void> => {
-    const error = new Error('error');
-    error.stack = undefined;
-    await writer.handle({ response, result: error });
-    expect(response._isEndCalled()).toBeTruthy();
-    expect(response._getStatusCode()).toBe(500);
-    expect(response._getData()).toMatch('Error: error');
-  });
-
-  it('ends its response with a newline if there is an error.', async(): Promise<void> => {
-    await writer.handle({ response, result: new Error('error') });
-    expect(response._isEndCalled()).toBeTruthy();
-    expect(response._getData().endsWith('\n')).toBeTruthy();
-  });
-});
diff --git a/test/unit/ldp/http/TextErrorHandler.test.ts b/test/unit/ldp/http/TextErrorHandler.test.ts
new file mode 100644
index 000000000..7c9121f9e
--- /dev/null
+++ b/test/unit/ldp/http/TextErrorHandler.test.ts
@@ -0,0 +1,60 @@
+import 'jest-rdf';
+import { DataFactory } from 'n3';
+import stringifyStream from 'stream-to-string';
+import { TextErrorHandler } from '../../../../src/ldp/http/TextErrorHandler';
+import { NotFoundHttpError } from '../../../../src/util/errors/NotFoundHttpError';
+import { HTTP, XSD } from '../../../../src/util/Vocabularies';
+import literal = DataFactory.literal;
+
+describe('A TextErrorHandler', (): void => {
+  // The error object can get modified by the handler
+  let error: Error;
+  let stack: string | undefined;
+  let handler: TextErrorHandler;
+
+  beforeEach(async(): Promise<void> => {
+    error = new NotFoundHttpError('not here');
+    ({ stack } = error);
+
+    handler = new TextErrorHandler(true);
+  });
+
+  it('can handle everything.', async(): Promise<void> => {
+    await expect(handler.canHandle({} as any)).resolves.toBeUndefined();
+  });
+
+  it('creates a text representation of the error.', async(): Promise<void> => {
+    const prom = handler.handle({ error } as any);
+    await expect(prom).resolves.toBeDefined();
+    const result = await prom;
+    expect(result.statusCode).toBe(404);
+    expect(result.metadata?.get(HTTP.terms.statusCodeNumber)).toEqualRdfTerm(literal(404, XSD.terms.integer));
+    expect(result.metadata?.contentType).toBe('text/plain');
+    const text = await stringifyStream(result.data!);
+    expect(text).toBe(`${stack}\n`);
+  });
+
+  it('concatenates name and message if there is no stack.', async(): Promise<void> => {
+    delete error.stack;
+    const prom = handler.handle({ error } as any);
+    await expect(prom).resolves.toBeDefined();
+    const result = await prom;
+    expect(result.statusCode).toBe(404);
+    expect(result.metadata?.get(HTTP.terms.statusCodeNumber)).toEqualRdfTerm(literal(404, XSD.terms.integer));
+    expect(result.metadata?.contentType).toBe('text/plain');
+    const text = await stringifyStream(result.data!);
+    expect(text).toBe(`NotFoundHttpError: not here\n`);
+  });
+
+  it('hides the stack trace if the option is disabled.', async(): Promise<void> => {
+    handler = new TextErrorHandler();
+    const prom = handler.handle({ error } as any);
+    await expect(prom).resolves.toBeDefined();
+    const result = await prom;
+    expect(result.statusCode).toBe(404);
+    expect(result.metadata?.get(HTTP.terms.statusCodeNumber)).toEqualRdfTerm(literal(404, XSD.terms.integer));
+    expect(result.metadata?.contentType).toBe('text/plain');
+    const text = await stringifyStream(result.data!);
+    expect(text).toBe(`NotFoundHttpError: not here\n`);
+  });
+});
diff --git a/test/unit/ldp/http/metadata/SlugParser.test.ts b/test/unit/ldp/http/metadata/SlugParser.test.ts
index 05c1a588a..3c415fef3 100644
--- a/test/unit/ldp/http/metadata/SlugParser.test.ts
+++ b/test/unit/ldp/http/metadata/SlugParser.test.ts
@@ -2,7 +2,7 @@ import { SlugParser } from '../../../../../src/ldp/http/metadata/SlugParser';
 import { RepresentationMetadata } from '../../../../../src/ldp/representation/RepresentationMetadata';
 import type { HttpRequest } from '../../../../../src/server/HttpRequest';
 import { BadRequestHttpError } from '../../../../../src/util/errors/BadRequestHttpError';
-import { HTTP } from '../../../../../src/util/Vocabularies';
+import { SOLID_HTTP } from '../../../../../src/util/Vocabularies';
 
 describe('A SlugParser', (): void => {
   const parser = new SlugParser();
@@ -30,6 +30,6 @@ describe('A SlugParser', (): void => {
     request.headers.slug = 'slugA';
     await expect(parser.handle({ request, metadata })).resolves.toBeUndefined();
     expect(metadata.quads()).toHaveLength(1);
-    expect(metadata.get(HTTP.slug)?.value).toBe('slugA');
+    expect(metadata.get(SOLID_HTTP.slug)?.value).toBe('slugA');
   });
 });
diff --git a/test/unit/ldp/operations/PostOperationHandler.test.ts b/test/unit/ldp/operations/PostOperationHandler.test.ts
index 89d0e84ea..92b718121 100644
--- a/test/unit/ldp/operations/PostOperationHandler.test.ts
+++ b/test/unit/ldp/operations/PostOperationHandler.test.ts
@@ -5,7 +5,7 @@ import type { ResourceIdentifier } from '../../../../src/ldp/representation/Reso
 import type { ResourceStore } from '../../../../src/storage/ResourceStore';
 import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError';
 import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
-import { HTTP } from '../../../../src/util/Vocabularies';
+import { SOLID_HTTP } from '../../../../src/util/Vocabularies';
 
 describe('A PostOperationHandler', (): void => {
   const store = {
@@ -31,7 +31,7 @@ describe('A PostOperationHandler', (): void => {
     const result = await handler.handle({ method: 'POST', body: { metadata }} as Operation);
     expect(result.statusCode).toBe(201);
     expect(result.metadata).toBeInstanceOf(RepresentationMetadata);
-    expect(result.metadata?.get(HTTP.location)?.value).toBe('newPath');
+    expect(result.metadata?.get(SOLID_HTTP.location)?.value).toBe('newPath');
     expect(result.data).toBeUndefined();
   });
 });
diff --git a/test/unit/server/BaseHttpServerFactory.test.ts b/test/unit/server/BaseHttpServerFactory.test.ts
index 100b84301..a49ee6c64 100644
--- a/test/unit/server/BaseHttpServerFactory.test.ts
+++ b/test/unit/server/BaseHttpServerFactory.test.ts
@@ -66,12 +66,12 @@ describe('A BaseHttpServerFactory', (): void => {
       await expect(request(server).get('/').expect(404)).resolves.toBeDefined();
     });
 
-    it('writes an error to the HTTP response.', async(): Promise<void> => {
+    it('writes an error to the HTTP response without the stack trace.', async(): Promise<void> => {
       handler.handleSafe.mockRejectedValueOnce(new Error('dummyError'));
 
       const res = await request(server).get('/').expect(500);
       expect(res.headers['content-type']).toBe('text/plain; charset=utf-8');
-      expect(res.text).toContain('dummyError');
+      expect(res.text).toBe('Error: dummyError\n');
     });
 
     it('does not write an error if the response had been started.', async(): Promise<void> => {
@@ -91,4 +91,29 @@ describe('A BaseHttpServerFactory', (): void => {
       expect(res.text).toContain('Unknown error.');
     });
   });
+
+  describe('with showStackTrace enabled', (): void => {
+    const httpOptions = {
+      http: true,
+      showStackTrace: true,
+    };
+
+    beforeAll(async(): Promise<void> => {
+      const factory = new BaseHttpServerFactory(handler, httpOptions);
+      server = factory.startServer(port);
+    });
+
+    afterAll(async(): Promise<void> => {
+      server.close();
+    });
+
+    it('throws unknown errors if its handler throw non-Error objects.', async(): Promise<void> => {
+      const error = new Error('dummyError');
+      handler.handleSafe.mockRejectedValueOnce(error);
+
+      const res = await request(server).get('/').expect(500);
+      expect(res.headers['content-type']).toBe('text/plain; charset=utf-8');
+      expect(res.text).toBe(`${error.stack}\n`);
+    });
+  });
 });
diff --git a/test/unit/storage/DataAccessorBasedStore.test.ts b/test/unit/storage/DataAccessorBasedStore.test.ts
index 405941d69..75d423958 100644
--- a/test/unit/storage/DataAccessorBasedStore.test.ts
+++ b/test/unit/storage/DataAccessorBasedStore.test.ts
@@ -22,7 +22,7 @@ import { SingleRootIdentifierStrategy } from '../../../src/util/identifiers/Sing
 import { trimTrailingSlashes } from '../../../src/util/PathUtil';
 import * as quadUtil from '../../../src/util/QuadUtil';
 import { guardedStreamFrom } from '../../../src/util/StreamUtil';
-import { CONTENT_TYPE, HTTP, LDP, PIM, RDF } from '../../../src/util/Vocabularies';
+import { CONTENT_TYPE, SOLID_HTTP, LDP, PIM, RDF } from '../../../src/util/Vocabularies';
 import quad = DataFactory.quad;
 import namedNode = DataFactory.namedNode;
 
@@ -277,7 +277,7 @@ describe('A DataAccessorBasedStore', (): void => {
     it('creates a URI based on the incoming slug.', async(): Promise<void> => {
       const resourceID = { path: root };
       representation.metadata.removeAll(RDF.type);
-      representation.metadata.add(HTTP.slug, 'newName');
+      representation.metadata.add(SOLID_HTTP.slug, 'newName');
       const result = await store.addResource(resourceID, representation);
       expect(result).toEqual({
         path: `${root}newName`,
@@ -286,7 +286,7 @@ describe('A DataAccessorBasedStore', (): void => {
 
     it('generates a new URI if adding the slug would create an existing URI.', async(): Promise<void> => {
       const resourceID = { path: root };
-      representation.metadata.add(HTTP.slug, 'newName');
+      representation.metadata.add(SOLID_HTTP.slug, 'newName');
       accessor.data[`${root}newName`] = representation;
       const result = await store.addResource(resourceID, representation);
       expect(result).not.toEqual({
@@ -300,7 +300,7 @@ describe('A DataAccessorBasedStore', (): void => {
     it('generates http://test.com/%26%26 when slug is &%26.', async(): Promise<void> => {
       const resourceID = { path: root };
       representation.metadata.removeAll(RDF.type);
-      representation.metadata.add(HTTP.slug, '&%26');
+      representation.metadata.add(SOLID_HTTP.slug, '&%26');
       const result = await store.addResource(resourceID, representation);
       expect(result).toEqual({ path: `${root}%26%26` });
     });
@@ -309,7 +309,7 @@ describe('A DataAccessorBasedStore', (): void => {
       const resourceID = { path: root };
       representation.metadata.removeAll(RDF.type);
       representation.data = guardedStreamFrom([ `` ]);
-      representation.metadata.add(HTTP.slug, 'sla/sh/es');
+      representation.metadata.add(SOLID_HTTP.slug, 'sla/sh/es');
       const result = store.addResource(resourceID, representation);
       await expect(result).rejects.toThrow(BadRequestHttpError);
       await expect(result).rejects.toThrow('Slugs should not contain slashes');
@@ -318,7 +318,7 @@ describe('A DataAccessorBasedStore', (): void => {
     it('errors if the slug would cause an auxiliary resource URI to be generated.', async(): Promise<void> => {
       const resourceID = { path: root };
       representation.metadata.removeAll(RDF.type);
-      representation.metadata.add(HTTP.slug, 'test.dummy');
+      representation.metadata.add(SOLID_HTTP.slug, 'test.dummy');
       const result = store.addResource(resourceID, representation);
       await expect(result).rejects.toThrow(ForbiddenHttpError);
       await expect(result).rejects.toThrow('Slug bodies that would result in an auxiliary resource are forbidden');
diff --git a/test/unit/storage/conversion/ErrorToQuadConverter.test.ts b/test/unit/storage/conversion/ErrorToQuadConverter.test.ts
new file mode 100644
index 000000000..f5582acca
--- /dev/null
+++ b/test/unit/storage/conversion/ErrorToQuadConverter.test.ts
@@ -0,0 +1,59 @@
+import 'jest-rdf';
+import arrayifyStream from 'arrayify-stream';
+import { DataFactory } from 'n3';
+import { BasicRepresentation } from '../../../../src/ldp/representation/BasicRepresentation';
+import { ErrorToQuadConverter } from '../../../../src/storage/conversion/ErrorToQuadConverter';
+import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError';
+import { InternalServerError } from '../../../../src/util/errors/InternalServerError';
+import { DC, SOLID_ERROR } from '../../../../src/util/Vocabularies';
+const { literal, namedNode, quad } = DataFactory;
+
+describe('An ErrorToQuadConverter', (): void => {
+  const identifier = { path: 'http://test.com/error' };
+  const converter = new ErrorToQuadConverter();
+  const preferences = {};
+
+  it('supports going from errors to quads.', async(): Promise<void> => {
+    await expect(converter.getInputTypes()).resolves.toEqual({ 'internal/error': 1 });
+    await expect(converter.getOutputTypes()).resolves.toEqual({ 'internal/quads': 1 });
+  });
+
+  it('does not support multiple errors.', async(): Promise<void> => {
+    const representation = new BasicRepresentation([ new Error(), new Error() ], 'internal/error', false);
+    const prom = converter.handle({ identifier, representation, preferences });
+    await expect(prom).rejects.toThrow('Only single errors are supported.');
+    await expect(prom).rejects.toThrow(InternalServerError);
+  });
+
+  it('adds triples for all error fields.', async(): Promise<void> => {
+    const error = new BadRequestHttpError('error text');
+    const representation = new BasicRepresentation([ error ], 'internal/error', false);
+    const prom = converter.handle({ identifier, representation, preferences });
+    await expect(prom).resolves.toBeDefined();
+    const result = await prom;
+    expect(result.binary).toBe(false);
+    expect(result.metadata.contentType).toBe('internal/quads');
+    const quads = await arrayifyStream(result.data);
+    expect(quads).toBeRdfIsomorphic([
+      quad(namedNode(identifier.path), DC.terms.title, literal('BadRequestHttpError')),
+      quad(namedNode(identifier.path), DC.terms.description, literal('error text')),
+      quad(namedNode(identifier.path), SOLID_ERROR.terms.stack, literal(error.stack!)),
+    ]);
+  });
+
+  it('only adds stack if it is defined.', async(): Promise<void> => {
+    const error = new BadRequestHttpError('error text');
+    delete error.stack;
+    const representation = new BasicRepresentation([ error ], 'internal/error', false);
+    const prom = converter.handle({ identifier, representation, preferences });
+    await expect(prom).resolves.toBeDefined();
+    const result = await prom;
+    expect(result.binary).toBe(false);
+    expect(result.metadata.contentType).toBe('internal/quads');
+    const quads = await arrayifyStream(result.data);
+    expect(quads).toBeRdfIsomorphic([
+      quad(namedNode(identifier.path), DC.terms.title, literal('BadRequestHttpError')),
+      quad(namedNode(identifier.path), DC.terms.description, literal('error text')),
+    ]);
+  });
+});
diff --git a/test/unit/util/errors/ErrorUtil.test.ts b/test/unit/util/errors/ErrorUtil.test.ts
new file mode 100644
index 000000000..8214c699e
--- /dev/null
+++ b/test/unit/util/errors/ErrorUtil.test.ts
@@ -0,0 +1,34 @@
+import { assertNativeError, getStatusCode, isNativeError } from '../../../../src/util/errors/ErrorUtil';
+import { NotFoundHttpError } from '../../../../src/util/errors/NotFoundHttpError';
+
+describe('ErrorUtil', (): void => {
+  describe('#isNativeError', (): void => {
+    it('returns true on native errors.', async(): Promise<void> => {
+      expect(isNativeError(new Error('error'))).toBe(true);
+    });
+
+    it('returns false on other values.', async(): Promise<void> => {
+      expect(isNativeError('apple')).toBe(false);
+    });
+  });
+
+  describe('#assertNativeError', (): void => {
+    it('returns undefined on native errors.', async(): Promise<void> => {
+      expect(assertNativeError(new Error('error'))).toBeUndefined();
+    });
+
+    it('throws on other values.', async(): Promise<void> => {
+      expect((): void => assertNativeError('apple')).toThrow('apple');
+    });
+  });
+
+  describe('#getStatusCode', (): void => {
+    it('returns the corresponding status code for HttpErrors.', async(): Promise<void> => {
+      expect(getStatusCode(new NotFoundHttpError())).toBe(404);
+    });
+
+    it('returns 500 for other errors.', async(): Promise<void> => {
+      expect(getStatusCode(new Error('404'))).toBe(500);
+    });
+  });
+});