diff --git a/django/views/debug.py b/django/views/debug.py
--- a/django/views/debug.py
+++ b/django/views/debug.py
@@ -59,8 +59,12 @@
     the values returned from sys.exc_info() and friends.
     """
     reporter = ExceptionReporter(request, exc_type, exc_value, tb)
-    html = reporter.get_traceback_html()
-    return HttpResponseServerError(html, mimetype='text/html')
+    if request.is_ajax():
+        text = reporter.get_traceback_text()
+        return HttpResponseServerError(text, mimetype='text/plain')
+    else:
+        html = reporter.get_traceback_html()
+        return HttpResponseServerError(html, mimetype='text/html')
 
 # Cache for the default exception reporter filter instance.
 default_exception_reporter_filter = None
@@ -201,8 +205,8 @@
             self.exc_value = Exception('Deprecated String Exception: %r' % self.exc_type)
             self.exc_type = type(self.exc_value)
 
-    def get_traceback_html(self):
-        "Return HTML code for traceback."
+    def get_traceback_data(self):
+        "Return a Context instance containing traceback information."
 
         if self.exc_type and issubclass(self.exc_type, TemplateDoesNotExist):
             from django.template.loader import template_source_loaders
@@ -240,8 +244,7 @@
                 unicode_str = self.exc_value.args[1]
                 unicode_hint = smart_unicode(unicode_str[max(start-5, 0):min(end+5, len(unicode_str))], 'ascii', errors='replace')
         from django import get_version
-        t = Template(TECHNICAL_500_TEMPLATE, name='Technical 500 template')
-        c = Context({
+        c = {
             'is_email': self.is_email,
             'unicode_hint': unicode_hint,
             'frames': frames,
@@ -256,7 +259,7 @@
             'template_info': self.template_info,
             'template_does_not_exist': self.template_does_not_exist,
             'loader_debug_info': self.loader_debug_info,
-        })
+        }
         # Check whether exception info is available
         if self.exc_type:
             c['exception_type'] = self.exc_type.__name__
@@ -264,6 +267,18 @@
             c['exception_value'] = smart_unicode(self.exc_value, errors='replace')
         if frames:
             c['lastframe'] = frames[-1]
+        return c
+
+    def get_traceback_html(self):
+        "Return HTML version of debug 500 HTTP error page."
+        t = Template(TECHNICAL_500_TEMPLATE, name='Technical 500 template')
+        c = Context(self.get_traceback_data())
+        return t.render(c)
+
+    def get_traceback_text(self):
+        "Return plain text version of debug 500 HTTP error page."
+        t = Template(TECHNICAL_500_TEXT_TEMPLATE, name='Technical 500 template')
+        c = Context(self.get_traceback_data(), autoescape=False)
         return t.render(c)
 
     def get_template_exception_info(self):
@@ -890,6 +905,67 @@
 </html>
 """
 
+TECHNICAL_500_TEXT_TEMPLATE = """{% firstof exception_type 'Report' %}{% if request %} at {{ request.path_info }}{% endif %}
+{% firstof exception_value 'No exception supplied' %}
+{% if request %}
+Request Method: {{ request.META.REQUEST_METHOD }}
+Request URL: {{ request.build_absolute_uri }}{% endif %}
+Django Version: {{ django_version_info }}
+Python Executable: {{ sys_executable }}
+Python Version: {{ sys_version_info }}
+Python Path: {{ sys_path }}
+Server time: {{server_time|date:"r"}}
+Installed Applications:
+{{ settings.INSTALLED_APPS|pprint }}
+Installed Middleware:
+{{ settings.MIDDLEWARE_CLASSES|pprint }}
+{% if template_does_not_exist %}Template loader Error:
+{% if loader_debug_info %}Django tried loading these templates, in this order:
+{% for loader in loader_debug_info %}Using loader {{ loader.loader }}:
+{% for t in loader.templates %}{{ t.name }} (File {% if t.exists %}exists{% else %}does not exist{% endif %})
+{% endfor %}{% endfor %}
+{% else %}Django couldn't find any templates because your TEMPLATE_LOADERS setting is empty!
+{% endif %}
+{% endif %}{% if template_info %}
+Template error:
+In template {{ template_info.name }}, error at line {{ template_info.line }}
+   {{ template_info.message }}{% for source_line in template_info.source_lines %}{% ifequal source_line.0 template_info.line %}
+   {{ source_line.0 }} : {{ template_info.before }} {{ template_info.during }} {{ template_info.after }}
+{% else %}
+   {{ source_line.0 }} : {{ source_line.1 }}
+   {% endifequal %}{% endfor %}{% endif %}{% if frames %}
+Traceback:
+{% for frame in frames %}File "{{ frame.filename }}" in {{ frame.function }}
+{% if frame.context_line %}  {{ frame.lineno }}. {{ frame.context_line }}{% endif %}
+{% endfor %}
+{% if exception_type %}Exception Type: {{ exception_type }}{% if request %} at {{ request.path_info }}{% endif %}
+{% if exception_value %}Exception Value: {{ exception_value }}{% endif %}{% endif %}{% endif %}
+{% if request %}Request information:
+GET:{% for k, v in request.GET.items %}
+{{ k }} = {{ v|stringformat:"r" }}{% empty %} No GET data{% endfor %}
+
+POST:{% for k, v in filtered_POST.items %}
+{{ k }} = {{ v|stringformat:"r" }}{% empty %} No POST data{% endfor %}
+
+FILES:{% for k, v in request.FILES.items %}
+{{ k }} = {{ v|stringformat:"r" }}{% empty %} No FILES data{% endfor %}
+
+COOKIES:{% for k, v in request.COOKIES.items %}
+{{ k }} = {{ v|stringformat:"r" }}{% empty %} No cookie data{% endfor %}
+
+META:{% for k, v in request.META.items|dictsort:"0" %}
+{{ k }} = {{ v|stringformat:"r" }}{% endfor %}
+{% else %}Request data not supplied
+{% endif %}
+Settings:
+Using settings module {{ settings.SETTINGS_MODULE }}{% for k, v in settings.items|dictsort:"0" %}
+{{ k }} = {{ v|stringformat:"r" }}{% endfor %}
+
+You're seeing this error because you have DEBUG = True in your
+Django settings file. Change that to False, and Django will
+display a standard 500 page.
+"""
+
 TECHNICAL_404_TEMPLATE = """
 <!DOCTYPE html>
 <html lang="en">
diff --git a/docs/releases/1.4.txt b/docs/releases/1.4.txt
--- a/docs/releases/1.4.txt
+++ b/docs/releases/1.4.txt
@@ -315,6 +315,17 @@
   be able to retrieve a translation string without displaying it but setting
   a template context variable instead.
 
+* A new plaintext version of the HTTP 500 status code internal error page
+  served when :setting:`DEBUG` is ``True`` is now sent to the client when
+  Django detect that the request has originated in JavaScript code
+  (:meth:`~django.http.HttpRequest.is_ajax` is used for this).
+
+  Similarly to its HTML counterpart, it contains a collection of different
+  pieces of useful information about the state of the web application.
+
+  This should make it easier to read when debugging interaction with
+  client-side Javascript code.
+
 .. _backwards-incompatible-changes-1.4:
 
 Backwards incompatible changes in 1.4
diff --git a/tests/regressiontests/views/tests/debug.py b/tests/regressiontests/views/tests/debug.py
--- a/tests/regressiontests/views/tests/debug.py
+++ b/tests/regressiontests/views/tests/debug.py
@@ -171,45 +171,104 @@
         self.assertIn('<p>Request data not supplied</p>', html)
 
 
-class ExceptionReporterFilterTests(TestCase):
-    """
-    Ensure that sensitive information can be filtered out of error reports.
-    Refs #14614.
-    """
+class PlainTextReportTests(TestCase):
     rf = RequestFactory()
+
+    def test_request_and_exception(self):
+        "A simple exception report can be generated"
+        try:
+            request = self.rf.get('/test_view/')
+            raise ValueError("Can't find my keys")
+        except ValueError:
+            exc_type, exc_value, tb = sys.exc_info()
+        reporter = ExceptionReporter(request, exc_type, exc_value, tb)
+        text = reporter.get_traceback_text()
+        self.assertIn('ValueError at /test_view/', text)
+        self.assertIn("Can't find my keys", text)
+        self.assertIn('Request Method:', text)
+        self.assertIn('Request URL:', text)
+        self.assertIn('Exception Type:', text)
+        self.assertIn('Exception Value:', text)
+        self.assertIn('Traceback:', text)
+        self.assertIn('Request information:', text)
+        self.assertNotIn('Request data not supplied', text)
+
+    def test_no_request(self):
+        "An exception report can be generated without request"
+        try:
+            raise ValueError("Can't find my keys")
+        except ValueError:
+            exc_type, exc_value, tb = sys.exc_info()
+        reporter = ExceptionReporter(None, exc_type, exc_value, tb)
+        text = reporter.get_traceback_text()
+        self.assertIn('ValueError', text)
+        self.assertIn("Can't find my keys", text)
+        self.assertNotIn('Request Method:', text)
+        self.assertNotIn('Request URL:', text)
+        self.assertIn('Exception Type:', text)
+        self.assertIn('Exception Value:', text)
+        self.assertIn('Traceback:', text)
+        self.assertIn('Request data not supplied', text)
+
+    def test_no_exception(self):
+        "An exception report can be generated for just a request"
+        request = self.rf.get('/test_view/')
+        reporter = ExceptionReporter(request, None, None, None)
+        text = reporter.get_traceback_text()
+
+    def test_request_and_message(self):
+        "A message can be provided in addition to a request"
+        request = self.rf.get('/test_view/')
+        reporter = ExceptionReporter(request, None, "I'm a little teapot", None)
+        text = reporter.get_traceback_text()
+
+    def test_message_only(self):
+        reporter = ExceptionReporter(None, None, "I'm a little teapot", None)
+        text = reporter.get_traceback_text()
+
+
+class ExceptionReportTestMixin(object):
+
+    # Mixin used in the ExceptionReporterFilterTests and
+    # AjaxResponseExceptionReporterFilter tests below
+
     breakfast_data = {'sausage-key': 'sausage-value',
                       'baked-beans-key': 'baked-beans-value',
                       'hash-brown-key': 'hash-brown-value',
                       'bacon-key': 'bacon-value',}
 
-    def verify_unsafe_response(self, view):
+    def verify_unsafe_response(self, view, check_for_vars=True):
         """
         Asserts that potentially sensitive info are displayed in the response.
         """
         request = self.rf.post('/some_url/', self.breakfast_data)
         response = view(request)
-        # All variables are shown.
-        self.assertContains(response, 'cooked_eggs', status_code=500)
-        self.assertContains(response, 'scrambled', status_code=500)
-        self.assertContains(response, 'sauce', status_code=500)
-        self.assertContains(response, 'worcestershire', status_code=500)
+        if check_for_vars:
+            # All variables are shown.
+            self.assertContains(response, 'cooked_eggs', status_code=500)
+            self.assertContains(response, 'scrambled', status_code=500)
+            self.assertContains(response, 'sauce', status_code=500)
+            self.assertContains(response, 'worcestershire', status_code=500)
+
         for k, v in self.breakfast_data.items():
             # All POST parameters are shown.
             self.assertContains(response, k, status_code=500)
             self.assertContains(response, v, status_code=500)
 
-    def verify_safe_response(self, view):
+    def verify_safe_response(self, view, check_for_vars=True):
         """
         Asserts that certain sensitive info are not displayed in the response.
         """
         request = self.rf.post('/some_url/', self.breakfast_data)
         response = view(request)
-        # Non-sensitive variable's name and value are shown.
-        self.assertContains(response, 'cooked_eggs', status_code=500)
-        self.assertContains(response, 'scrambled', status_code=500)
-        # Sensitive variable's name is shown but not its value.
-        self.assertContains(response, 'sauce', status_code=500)
-        self.assertNotContains(response, 'worcestershire', status_code=500)
+        if check_for_vars:
+            # Non-sensitive variable's name and value are shown.
+            self.assertContains(response, 'cooked_eggs', status_code=500)
+            self.assertContains(response, 'scrambled', status_code=500)
+            # Sensitive variable's name is shown but not its value.
+            self.assertContains(response, 'sauce', status_code=500)
+            self.assertNotContains(response, 'worcestershire', status_code=500)
+
         for k, v in self.breakfast_data.items():
             # All POST parameters' names are shown.
             self.assertContains(response, k, status_code=500)
@@ -220,17 +279,19 @@
         self.assertNotContains(response, 'sausage-value', status_code=500)
         self.assertNotContains(response, 'bacon-value', status_code=500)
 
-    def verify_paranoid_response(self, view):
+    def verify_paranoid_response(self, view, check_for_vars=True):
         """
         Asserts that no variables or POST parameters are displayed in the response.
         """
         request = self.rf.post('/some_url/', self.breakfast_data)
         response = view(request)
-        # Show variable names but not their values.
-        self.assertContains(response, 'cooked_eggs', status_code=500)
-        self.assertNotContains(response, 'scrambled', status_code=500)
-        self.assertContains(response, 'sauce', status_code=500)
-        self.assertNotContains(response, 'worcestershire', status_code=500)
+        if check_for_vars:
+            # Show variable names but not their values.
+            self.assertContains(response, 'cooked_eggs', status_code=500)
+            self.assertNotContains(response, 'scrambled', status_code=500)
+            self.assertContains(response, 'sauce', status_code=500)
+            self.assertNotContains(response, 'worcestershire', status_code=500)
+
         for k, v in self.breakfast_data.items():
             # All POST parameters' names are shown.
             self.assertContains(response, k, status_code=500)
@@ -303,6 +364,14 @@
                 # No POST parameters' values are shown.
                 self.assertNotIn(v, email.body)
 
+
+class ExceptionReporterFilterTests(TestCase, ExceptionReportTestMixin):
+    """
+    Ensure that sensitive information can be filtered out of error reports.
+    Refs #14614.
+    """
+    rf = RequestFactory()
+
     def test_non_sensitive_request(self):
         """
         Ensure that everything (request info and frame variables) can bee seen
@@ -354,3 +423,62 @@
         with self.settings(DEBUG=False):
             self.verify_unsafe_response(custom_exception_reporter_filter_view)
             self.verify_unsafe_email(custom_exception_reporter_filter_view)
+
+
+class AjaxResponseExceptionReporterFilter(TestCase, ExceptionReportTestMixin):
+    """
+    Ensure that sensitive information can be filtered out of error reports.
+
+    Here we specifically test the plain text 500 error page generated when it
+    has been detected the request was sent by JS code. We don't check
+    (non)existence of views variables in the traceback information section of
+    the response content because they aren't included in these error pages.
+    Refs #14614.
+    """
+    rf = RequestFactory(HTTP_X_REQUESTED_WITH='XMLHttpRequest')
+
+    def test_non_sensitive_request(self):
+        """
+        Ensure that request info can bee seen in the default error reports for
+        non-sensitive requests.
+        """
+        with self.settings(DEBUG=True):
+            self.verify_unsafe_response(non_sensitive_view, check_for_vars=False)
+
+        with self.settings(DEBUG=False):
+            self.verify_unsafe_response(non_sensitive_view, check_for_vars=False)
+
+    def test_sensitive_request(self):
+        """
+        Ensure that sensitive POST parameters cannot be seen in the default
+        error reports for sensitive requests.
+        """
+        with self.settings(DEBUG=True):
+            self.verify_unsafe_response(sensitive_view, check_for_vars=False)
+
+        with self.settings(DEBUG=False):
+            self.verify_safe_response(sensitive_view, check_for_vars=False)
+
+    def test_paranoid_request(self):
+        """
+        Ensure that no POST parameters can be seen in the default error reports
+        for "paranoid" requests.
+        """
+        with self.settings(DEBUG=True):
+            self.verify_unsafe_response(paranoid_view, check_for_vars=False)
+
+        with self.settings(DEBUG=False):
+            self.verify_paranoid_response(paranoid_view, check_for_vars=False)
+
+    def test_custom_exception_reporter_filter(self):
+        """
+        Ensure that it's possible to assign an exception reporter filter to
+        the request to bypass the one set in DEFAULT_EXCEPTION_REPORTER_FILTER.
+        """
+        with self.settings(DEBUG=True):
+            self.verify_unsafe_response(custom_exception_reporter_filter_view,
+                check_for_vars=False)
+
+        with self.settings(DEBUG=False):
+            self.verify_unsafe_response(custom_exception_reporter_filter_view,
+                check_for_vars=False)
