diff --git a/NEWS.rst b/NEWS.rst
index 6f3d53cf0..20d45ef85 100644
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -21,6 +21,7 @@ New Features
 
 Bug Fixes
 ------------------------------
+* Cleaned up syntax and compiler errors
 * Fixed issue with empty arguments in `defmain`.
 * `require` now compiles to Python AST.
 * Fixed circular `require`s.
diff --git a/docs/language/cli.rst b/docs/language/cli.rst
index 2c8a1f7fb..e59640d23 100644
--- a/docs/language/cli.rst
+++ b/docs/language/cli.rst
@@ -48,12 +48,6 @@ Command Line Options
    `--spy` only works on REPL mode.
    .. versionadded:: 0.9.11
 
-.. cmdoption:: --show-tracebacks
-
-   Print extended tracebacks for Hy exceptions.
-
-   .. versionadded:: 0.9.12
-
 .. cmdoption:: --repl-output-fn
 
    Format REPL output using specific function (e.g., hy.contrib.hy-repr.hy-repr)
diff --git a/hy/__init__.py b/hy/__init__.py
index f188b6419..eb1d91c1d 100644
--- a/hy/__init__.py
+++ b/hy/__init__.py
@@ -5,6 +5,16 @@
     __version__ = 'unknown'
 
 
+def _initialize_env_var(env_var, default_val):
+    import os, distutils.util
+    try:
+        res = bool(distutils.util.strtobool(
+            os.environ.get(env_var, str(default_val))))
+    except ValueError as e:
+        res = default_val
+    return res
+
+
 from hy.models import HyExpression, HyInteger, HyKeyword, HyComplex, HyString, HyBytes, HySymbol, HyFloat, HyDict, HyList, HySet  # NOQA
 
 
diff --git a/hy/_compat.py b/hy/_compat.py
index bd9390f85..92aa39230 100644
--- a/hy/_compat.py
+++ b/hy/_compat.py
@@ -6,7 +6,7 @@
     import __builtin__ as builtins
 except ImportError:
     import builtins  # NOQA
-import sys, keyword
+import sys, keyword, textwrap
 
 PY3 = sys.version_info[0] >= 3
 PY35 = sys.version_info >= (3, 5)
@@ -22,11 +22,60 @@
 long_type    = int   if PY3 else long         # NOQA
 string_types = str   if PY3 else basestring   # NOQA
 
+#
+# Inspired by the same-named `six` functions.
+#
 if PY3:
-    exec('def raise_empty(t, *args): raise t(*args) from None')
+    raise_src = textwrap.dedent('''
+    def raise_from(value, from_value):
+        raise value from from_value
+    ''')
+
+    def reraise(exc_type, value, traceback=None):
+        try:
+            raise value.with_traceback(traceback)
+        finally:
+            traceback = None
+
+    code_obj_args = ['argcount', 'kwonlyargcount', 'nlocals', 'stacksize',
+                     'flags', 'code', 'consts', 'names', 'varnames',
+                     'filename', 'name', 'firstlineno', 'lnotab', 'freevars',
+                     'cellvars']
 else:
-    def raise_empty(t, *args):
-        raise t(*args)
+    def raise_from(value, from_value=None):
+        raise value
+
+    raise_src = textwrap.dedent('''
+    def reraise(exc_type, value, traceback=None):
+        try:
+            raise exc_type, value, traceback
+        finally:
+            traceback = None
+    ''')
+
+    code_obj_args = ['argcount', 'nlocals', 'stacksize', 'flags', 'code',
+                     'consts', 'names', 'varnames', 'filename', 'name',
+                     'firstlineno', 'lnotab', 'freevars', 'cellvars']
+
+raise_code = compile(raise_src, __file__, 'exec')
+exec(raise_code)
+
+
+def rename_function(func, new_name):
+    """Creates a copy of a function and [re]sets the name at the code-object
+    level.
+    """
+    c = func.__code__
+    new_code = type(c)(*[getattr(c, 'co_{}'.format(a))
+                         if a != 'name' else str(new_name)
+                         for a in code_obj_args])
+
+    _fn = type(func)(new_code, func.__globals__, str(new_name),
+                     func.__defaults__, func.__closure__)
+    _fn.__dict__.update(func.__dict__)
+
+    return _fn
+
 
 def isidentifier(x):
     if x in ('True', 'False', 'None', 'print'):
diff --git a/hy/cmdline.py b/hy/cmdline.py
index 4c2e1c335..f65379d50 100644
--- a/hy/cmdline.py
+++ b/hy/cmdline.py
@@ -12,16 +12,25 @@
 import io
 import importlib
 import py_compile
+import traceback
 import runpy
 import types
+import time
+import linecache
+import hashlib
+import codeop
 
 import astor.code_gen
 
 import hy
+
 from hy.lex import hy_parse, mangle
-from hy.lex.exceptions import LexException, PrematureEndOfInput
-from hy.compiler import HyASTCompiler, hy_compile, hy_eval
-from hy.errors import HyTypeError
+from contextlib import contextmanager
+from hy.lex.exceptions import PrematureEndOfInput
+from hy.compiler import (HyASTCompiler, hy_eval, hy_compile,
+                         hy_ast_compile_flags)
+from hy.errors import (HyLanguageError, HyRequireError, HyMacroExpansionError,
+                       filtered_hy_exceptions, hy_exc_handler)
 from hy.importer import runhy
 from hy.completer import completion, Completer
 from hy.macros import macro, require
@@ -29,6 +38,11 @@
 from hy._compat import builtins, PY3, FileNotFoundError
 
 
+sys.last_type = None
+sys.last_value = None
+sys.last_traceback = None
+
+
 class HyQuitter(object):
     def __init__(self, name):
         self.name = name
@@ -49,30 +63,188 @@ def __call__(self, code=None):
 builtins.quit = HyQuitter('quit')
 builtins.exit = HyQuitter('exit')
 
+@contextmanager
+def extend_linecache(add_cmdline_cache):
+    _linecache_checkcache = linecache.checkcache
+
+    def _cmdline_checkcache(*args):
+        _linecache_checkcache(*args)
+        linecache.cache.update(add_cmdline_cache)
+
+    linecache.checkcache = _cmdline_checkcache
+    yield
+    linecache.checkcache = _linecache_checkcache
+
+
+_codeop_maybe_compile = codeop._maybe_compile
+
+
+def _hy_maybe_compile(compiler, source, filename, symbol):
+    """The `codeop` version of this will compile the same source multiple
+    times, and, since we have macros and things like `eval-and-compile`, we
+    can't allow that.
+    """
+    if not isinstance(compiler, HyCompile):
+        return _codeop_maybe_compile(compiler, source, filename, symbol)
+
+    for line in source.split("\n"):
+        line = line.strip()
+        if line and line[0] != ';':
+            # Leave it alone (could do more with Hy syntax)
+            break
+    else:
+        if symbol != "eval":
+            # Replace it with a 'pass' statement (i.e. tell the compiler to do
+            # nothing)
+            source = "pass"
+
+    return compiler(source, filename, symbol)
+
+
+codeop._maybe_compile = _hy_maybe_compile
+
+
+class HyCompile(codeop.Compile, object):
+    """This compiler uses `linecache` like
+    `IPython.core.compilerop.CachingCompiler`.
+    """
+
+    def __init__(self, module, locals, ast_callback=None,
+                 hy_compiler=None, cmdline_cache={}):
+        self.module = module
+        self.locals = locals
+        self.ast_callback = ast_callback
+        self.hy_compiler = hy_compiler
+
+        super(HyCompile, self).__init__()
+
+        self.flags |= hy_ast_compile_flags
+
+        self.cmdline_cache = cmdline_cache
+
+    def _cache(self, source, name):
+        entry = (len(source),
+                 time.time(),
+                 [line + '\n' for line in source.splitlines()],
+                 name)
+
+        linecache.cache[name] = entry
+        self.cmdline_cache[name] = entry
+
+    def _update_exc_info(self):
+            self.locals['_hy_last_type'] = sys.last_type
+            self.locals['_hy_last_value'] = sys.last_value
+            # Skip our frame.
+            sys.last_traceback = getattr(sys.last_traceback, 'tb_next',
+                                         sys.last_traceback)
+            self.locals['_hy_last_traceback'] = sys.last_traceback
+
+    def __call__(self, source, filename="<input>", symbol="single"):
+
+        if source == 'pass':
+            # We need to return a no-op to signal that no more input is needed.
+            return (compile(source, filename, symbol),) * 2
+
+        hash_digest = hashlib.sha1(source.encode("utf-8").strip()).hexdigest()
+        name = '{}-{}'.format(filename.strip('<>'), hash_digest)
+
+        try:
+            hy_ast = hy_parse(source, filename=name)
+        except Exception:
+            # Capture a traceback without the compiler/REPL frames.
+            sys.last_type, sys.last_value, sys.last_traceback = sys.exc_info()
+            self._update_exc_info()
+            raise
+
+        self._cache(source, name)
+
+        try:
+            hy_ast = hy_parse(source, filename=filename)
+            root_ast = ast.Interactive if symbol == 'single' else ast.Module
+
+            # Our compiler doesn't correspond to a real, fixed source file, so
+            # we need to [re]set these.
+            self.hy_compiler.filename = filename
+            self.hy_compiler.source = source
+            exec_ast, eval_ast = hy_compile(hy_ast, self.module, root=root_ast,
+                                            get_expr=True,
+                                            compiler=self.hy_compiler,
+                                            filename=filename, source=source)
+
+            if self.ast_callback:
+                self.ast_callback(exec_ast, eval_ast)
+
+            exec_code = super(HyCompile, self).__call__(exec_ast, name, symbol)
+            eval_code = super(HyCompile, self).__call__(eval_ast, name, 'eval')
+
+        except HyLanguageError:
+            # Hy will raise exceptions during compile-time that Python would
+            # raise during run-time (e.g. import errors for `require`).  In
+            # order to work gracefully with the Python world, we convert such
+            # Hy errors to code that purposefully reraises those exceptions in
+            # the places where Python code expects them.
+            sys.last_type, sys.last_value, sys.last_traceback = sys.exc_info()
+            self._update_exc_info()
+            exec_code = super(HyCompile, self).__call__(
+                'import hy._compat; hy._compat.reraise('
+                '_hy_last_type, _hy_last_value, _hy_last_traceback)',
+                name, symbol)
+            eval_code = super(HyCompile, self).__call__('None', name, 'eval')
+
+        return exec_code, eval_code
+
+
+class HyCommandCompiler(codeop.CommandCompiler, object):
+    def __init__(self, *args, **kwargs):
+        self.compiler = HyCompile(*args, **kwargs)
+
+    def __call__(self, *args, **kwargs):
+        try:
+            return super(HyCommandCompiler, self).__call__(*args, **kwargs)
+        except PrematureEndOfInput:
+            # We have to do this here, because `codeop._maybe_compile` won't
+            # take `None` for a return value (at least not in Python 2.7) and
+            # this exception type is also a `SyntaxError`, so it will be caught
+            # by `code.InteractiveConsole` base methods before it reaches our
+            # `runsource`.
+            return None
+
 
 class HyREPL(code.InteractiveConsole, object):
     def __init__(self, spy=False, output_fn=None, locals=None,
-                 filename="<input>"):
+                 filename="<stdin>"):
 
+        # Create a proper module for this REPL so that we can obtain it easily
+        # (e.g. using `importlib.import_module`).
+        # We let `InteractiveConsole` initialize `self.locals` when it's
+        # `None`.
         super(HyREPL, self).__init__(locals=locals,
                                      filename=filename)
 
-        # Create a proper module for this REPL so that we can obtain it easily
-        # (e.g. using `importlib.import_module`).
-        # Also, make sure it's properly introduced to `sys.modules` and
-        # consistently use its namespace as `locals` from here on.
         module_name = self.locals.get('__name__', '__console__')
+        # Make sure our newly created module is properly introduced to
+        # `sys.modules`, and consistently use its namespace as `self.locals`
+        # from here on.
         self.module = sys.modules.setdefault(module_name,
                                              types.ModuleType(module_name))
         self.module.__dict__.update(self.locals)
         self.locals = self.module.__dict__
 
         # Load cmdline-specific macros.
-        require('hy.cmdline', module_name, assignments='ALL')
+        require('hy.cmdline', self.module, assignments='ALL')
 
         self.hy_compiler = HyASTCompiler(self.module)
 
+        self.cmdline_cache = {}
+        self.compile = HyCommandCompiler(self.module,
+                                         self.locals,
+                                         ast_callback=self.ast_callback,
+                                         hy_compiler=self.hy_compiler,
+                                         cmdline_cache=self.cmdline_cache)
+
         self.spy = spy
+        self.last_value = None
+        self.print_last_value = True
 
         if output_fn is None:
             self.output_fn = repr
@@ -90,64 +262,95 @@ def __init__(self, spy=False, output_fn=None, locals=None,
         self._repl_results_symbols = [mangle("*{}".format(i + 1)) for i in range(3)]
         self.locals.update({sym: None for sym in self._repl_results_symbols})
 
-    def runsource(self, source, filename='<input>', symbol='single'):
-        global SIMPLE_TRACEBACKS
+        # Allow access to the running REPL instance
+        self.locals['_hy_repl'] = self
 
-        def error_handler(e, use_simple_traceback=False):
-            self.locals[mangle("*e")] = e
-            if use_simple_traceback:
-                print(e, file=sys.stderr)
-            else:
-                self.showtraceback()
+    def ast_callback(self, exec_ast, eval_ast):
+        if self.spy:
+            try:
+                # Mush the two AST chunks into a single module for
+                # conversion into Python.
+                new_ast = ast.Module(exec_ast.body +
+                                     [ast.Expr(eval_ast.body)])
+                print(astor.to_source(new_ast))
+            except Exception:
+                msg = 'Exception in AST callback:\n{}\n'.format(
+                    traceback.format_exc())
+                self.write(msg)
+
+    def _error_wrap(self, error_fn, exc_info_override=False, *args, **kwargs):
+        sys.last_type, sys.last_value, sys.last_traceback = sys.exc_info()
+
+        if exc_info_override:
+            # Use a traceback that doesn't have the REPL frames.
+            sys.last_type = self.locals.get('_hy_last_type', sys.last_type)
+            sys.last_value = self.locals.get('_hy_last_value', sys.last_value)
+            sys.last_traceback = self.locals.get('_hy_last_traceback',
+                                                 sys.last_traceback)
+
+        # Sadly, this method in Python 2.7 ignores an overridden `sys.excepthook`.
+        if sys.excepthook is sys.__excepthook__:
+            error_fn(*args, **kwargs)
+        else:
+            sys.excepthook(sys.last_type, sys.last_value, sys.last_traceback)
+
+        self.locals[mangle("*e")] = sys.last_value
+
+    def showsyntaxerror(self, filename=None):
+        if filename is None:
+            filename = self.filename
+
+        self._error_wrap(super(HyREPL, self).showsyntaxerror,
+                         exc_info_override=True,
+                         filename=filename)
 
+    def showtraceback(self):
+        self._error_wrap(super(HyREPL, self).showtraceback)
+
+    def runcode(self, code):
         try:
-            try:
-                do = hy_parse(source)
-            except PrematureEndOfInput:
-                return True
-        except LexException as e:
-            if e.source is None:
-                e.source = source
-                e.filename = filename
-            error_handler(e, use_simple_traceback=True)
-            return False
+            eval(code[0], self.locals)
+            self.last_value = eval(code[1], self.locals)
+            # Don't print `None` values.
+            self.print_last_value = self.last_value is not None
+        except SystemExit:
+            raise
+        except Exception as e:
+            # Set this to avoid a print-out of the last value on errors.
+            self.print_last_value = False
+            self.showtraceback()
 
+    def runsource(self, source, filename='<stdin>', symbol='exec'):
         try:
-            def ast_callback(main_ast, expr_ast):
-                if self.spy:
-                    # Mush the two AST chunks into a single module for
-                    # conversion into Python.
-                    new_ast = ast.Module(main_ast.body +
-                                         [ast.Expr(expr_ast.body)])
-                    print(astor.to_source(new_ast))
-
-            value = hy_eval(do, self.locals,
-                            ast_callback=ast_callback,
-                            compiler=self.hy_compiler)
-        except HyTypeError as e:
-            if e.source is None:
-                e.source = source
-                e.filename = filename
-            error_handler(e, use_simple_traceback=SIMPLE_TRACEBACKS)
+            res = super(HyREPL, self).runsource(source, filename, symbol)
+        except (HyMacroExpansionError, HyRequireError):
+            # We need to handle these exceptions ourselves, because the base
+            # method only handles `OverflowError`, `SyntaxError` and
+            # `ValueError`.
+            self.showsyntaxerror(filename)
             return False
-        except Exception as e:
-            error_handler(e)
+        except (HyLanguageError):
+            # Our compiler will also raise `TypeError`s
+            self.showtraceback()
             return False
 
-        if value is not None:
-            # Shift exisitng REPL results
-            next_result = value
+        # Shift exisitng REPL results
+        if not res:
+            next_result = self.last_value
             for sym in self._repl_results_symbols:
                 self.locals[sym], next_result = next_result, self.locals[sym]
 
             # Print the value.
-            try:
-                output = self.output_fn(value)
-            except Exception as e:
-                error_handler(e)
-                return False
-            print(output)
-        return False
+            if self.print_last_value:
+                try:
+                    output = self.output_fn(self.last_value)
+                except Exception:
+                    self.showtraceback()
+                    return False
+
+                print(output)
+
+        return res
 
 
 @macro("koan")
@@ -202,23 +405,17 @@ def ideas_macro(ETname):
 """)])
 
 
-SIMPLE_TRACEBACKS = True
-
-
-def pretty_error(func, *args, **kw):
+def run_command(source, filename=None):
+    __main__ = importlib.import_module('__main__')
+    require("hy.cmdline", __main__, assignments="ALL")
     try:
-        return func(*args, **kw)
-    except (HyTypeError, LexException) as e:
-        if SIMPLE_TRACEBACKS:
-            print(e, file=sys.stderr)
-            sys.exit(1)
-        raise
-
-
-def run_command(source):
-    tree = hy_parse(source)
-    require("hy.cmdline", "__main__", assignments="ALL")
-    pretty_error(hy_eval, tree, None, importlib.import_module('__main__'))
+        tree = hy_parse(source, filename=filename)
+    except HyLanguageError:
+        hy_exc_handler(*sys.exc_info())
+        return 1
+
+    with filtered_hy_exceptions():
+        hy_eval(tree, None, __main__, filename=filename, source=source)
     return 0
 
 
@@ -231,9 +428,9 @@ def run_repl(hr=None, **kwargs):
         hr = HyREPL(**kwargs)
 
     namespace = hr.locals
-
-    with completion(Completer(namespace)):
-
+    with filtered_hy_exceptions(), \
+         extend_linecache(hr.cmdline_cache), \
+         completion(Completer(namespace)):
         hr.interact("{appname} {version} using "
                     "{py}({build}) {pyversion} on {os}".format(
                         appname=hy.__appname__,
@@ -260,10 +457,17 @@ def run_icommand(source, **kwargs):
             source = f.read()
         filename = source
     else:
-        filename = '<input>'
+        filename = '<string>'
 
     hr = HyREPL(**kwargs)
-    hr.runsource(source, filename=filename, symbol='single')
+    with filtered_hy_exceptions():
+        res = hr.runsource(source, filename=filename)
+
+    # If the command was prematurely ended, show an error (just like Python
+    # does).
+    if res:
+        hy_exc_handler(sys.last_type, sys.last_value, sys.last_traceback)
+
     return run_repl(hr)
 
 
@@ -300,9 +504,6 @@ def cmdline_handler(scriptname, argv):
                              "(e.g., hy.contrib.hy-repr.hy-repr)")
     parser.add_argument("-v", "--version", action="version", version=VERSION)
 
-    parser.add_argument("--show-tracebacks", action="store_true",
-                        help="show complete tracebacks for Hy exceptions")
-
     # this will contain the script/program name and any arguments for it.
     parser.add_argument('args', nargs=argparse.REMAINDER,
                         help=argparse.SUPPRESS)
@@ -327,10 +528,6 @@ def cmdline_handler(scriptname, argv):
 
     options = parser.parse_args(argv[1:])
 
-    if options.show_tracebacks:
-        global SIMPLE_TRACEBACKS
-        SIMPLE_TRACEBACKS = False
-
     if options.E:
         # User did "hy -E ..."
         _remove_python_envs()
@@ -340,7 +537,7 @@ def cmdline_handler(scriptname, argv):
 
     if options.command:
         # User did "hy -c ..."
-        return run_command(options.command)
+        return run_command(options.command, filename='<string>')
 
     if options.mod:
         # User did "hy -m ..."
@@ -356,7 +553,7 @@ def cmdline_handler(scriptname, argv):
     if options.args:
         if options.args[0] == "-":
             # Read the program from stdin
-            return run_command(sys.stdin.read())
+            return run_command(sys.stdin.read(), filename='<stdin>')
 
         else:
             # User did "hy <filename>"
@@ -371,12 +568,16 @@ def cmdline_handler(scriptname, argv):
 
             try:
                 sys.argv = options.args
-                runhy.run_path(filename, run_name='__main__')
+                with filtered_hy_exceptions():
+                    runhy.run_path(filename, run_name='__main__')
                 return 0
             except FileNotFoundError as e:
                 print("hy: Can't open file '{0}': [Errno {1}] {2}".format(
                       e.filename, e.errno, e.strerror), file=sys.stderr)
                 sys.exit(e.errno)
+            except HyLanguageError:
+                hy_exc_handler(*sys.exc_info())
+                sys.exit(1)
 
     # User did NOTHING!
     return run_repl(spy=options.spy, output_fn=options.repl_output_fn)
@@ -446,12 +647,16 @@ def hy2py_main():
     options = parser.parse_args(sys.argv[1:])
 
     if options.FILE is None or options.FILE == '-':
+        filename = '<stdin>'
         source = sys.stdin.read()
     else:
+        filename = options.FILE
         with io.open(options.FILE, 'r', encoding='utf-8') as source_file:
             source = source_file.read()
 
-    hst = pretty_error(hy_parse, source)
+    with filtered_hy_exceptions():
+        hst = hy_parse(source, filename=filename)
+
     if options.with_source:
         # need special printing on Windows in case the
         # codepage doesn't support utf-8 characters
@@ -466,7 +671,9 @@ def hy2py_main():
         print()
         print()
 
-    _ast = pretty_error(hy_compile, hst, '__main__')
+    with filtered_hy_exceptions():
+        _ast = hy_compile(hst, '__main__', filename=filename, source=source)
+
     if options.with_ast:
         if PY3 and platform.system() == "Windows":
             _print_for_windows(astor.dump_tree(_ast))
diff --git a/hy/compiler.py b/hy/compiler.py
index f3b55a3fa..08e0c98a1 100755
--- a/hy/compiler.py
+++ b/hy/compiler.py
@@ -9,20 +9,21 @@
 from hy.model_patterns import (FORM, SYM, KEYWORD, STR, sym, brackets, whole,
                                notpexpr, dolike, pexpr, times, Tag, tag, unpack)
 from funcparserlib.parser import some, many, oneplus, maybe, NoParseError
-from hy.errors import HyCompileError, HyTypeError
+from hy.errors import (HyCompileError, HyTypeError, HyLanguageError,
+                       HySyntaxError, HyEvalError, HyInternalError)
 
 from hy.lex import mangle, unmangle
 
-from hy._compat import (str_type, string_types, bytes_type, long_type, PY3,
-                        PY35, raise_empty)
+from hy._compat import (string_types, str_type, bytes_type, long_type, PY3,
+                        PY35, reraise)
 from hy.macros import require, load_macros, macroexpand, tag_macroexpand
 
 import hy.core
 
+import pkgutil
 import traceback
 import importlib
 import inspect
-import pkgutil
 import types
 import ast
 import sys
@@ -340,22 +341,33 @@ def is_unpack(kind, x):
 class HyASTCompiler(object):
     """A Hy-to-Python AST compiler"""
 
-    def __init__(self, module):
+    def __init__(self, module, filename=None, source=None):
         """
         Parameters
         ----------
         module: str or types.ModuleType
-            Module in which the Hy tree is evaluated.
+            Module name or object in which the Hy tree is evaluated.
+        filename: str, optional
+            The name of the file for the source to be compiled.
+            This is optional information for informative error messages and
+            debugging.
+        source: str, optional
+            The source for the file, if any, being compiled.  This is optional
+            information for informative error messages and debugging.
         """
         self.anon_var_count = 0
         self.imports = defaultdict(set)
         self.temp_if = None
 
         if not inspect.ismodule(module):
-            module = importlib.import_module(module)
+            self.module = importlib.import_module(module)
+        else:
+            self.module = module
 
-        self.module = module
-        self.module_name = module.__name__
+        self.module_name = self.module.__name__
+
+        self.filename = filename
+        self.source = source
 
         # Hy expects these to be present, so we prep the module for Hy
         # compilation.
@@ -431,10 +443,18 @@ def compile(self, tree):
             # nested; so let's re-raise this exception, let's not wrap it in
             # another HyCompileError!
             raise
-        except HyTypeError:
-            raise
+        except HyLanguageError as e:
+            # These are expected errors that should be passed to the user.
+            reraise(type(e), e, sys.exc_info()[2])
         except Exception as e:
-            raise_empty(HyCompileError, e, sys.exc_info()[2])
+            # These are unexpected errors that will--hopefully--never be seen
+            # by the user.
+            f_exc = traceback.format_exc()
+            exc_msg = "Internal Compiler Bug 😱\n⤷ {}".format(f_exc)
+            reraise(HyCompileError, HyCompileError(exc_msg), sys.exc_info()[2])
+
+    def _syntax_error(self, expr, message):
+        return HySyntaxError(message, expr, self.filename, self.source)
 
     def _compile_collect(self, exprs, with_kwargs=False, dict_display=False,
                          oldpy_unpack=False):
@@ -455,8 +475,8 @@ def _compile_collect(self, exprs, with_kwargs=False, dict_display=False,
 
             if not PY35 and oldpy_unpack and is_unpack("iterable", expr):
                 if oldpy_starargs:
-                    raise HyTypeError(expr, "Pythons < 3.5 allow only one "
-                                            "`unpack-iterable` per call")
+                    raise self._syntax_error(expr,
+                        "Pythons < 3.5 allow only one `unpack-iterable` per call")
                 oldpy_starargs = self.compile(expr[1])
                 ret += oldpy_starargs
                 oldpy_starargs = oldpy_starargs.force_expr
@@ -472,21 +492,20 @@ def _compile_collect(self, exprs, with_kwargs=False, dict_display=False,
                             expr, arg=None, value=ret.force_expr))
                 elif oldpy_unpack:
                     if oldpy_kwargs:
-                        raise HyTypeError(expr, "Pythons < 3.5 allow only one "
-                                                "`unpack-mapping` per call")
+                        raise self._syntax_error(expr,
+                            "Pythons < 3.5 allow only one `unpack-mapping` per call")
                     oldpy_kwargs = ret.force_expr
 
             elif with_kwargs and isinstance(expr, HyKeyword):
                 try:
                     value = next(exprs_iter)
                 except StopIteration:
-                    raise HyTypeError(expr,
-                                      "Keyword argument {kw} needs "
-                                      "a value.".format(kw=expr))
+                    raise self._syntax_error(expr,
+                        "Keyword argument {kw} needs a value.".format(kw=expr))
 
                 if not expr:
-                    raise HyTypeError(expr, "Can't call a function with the "
-                                            "empty keyword")
+                    raise self._syntax_error(expr,
+                        "Can't call a function with the empty keyword")
 
                 compiled_value = self.compile(value)
                 ret += compiled_value
@@ -527,8 +546,8 @@ def _storeize(self, expr, name, func=None):
 
         if isinstance(name, Result):
             if not name.is_expr():
-                raise HyTypeError(expr,
-                                  "Can't assign or delete a non-expression")
+                raise self._syntax_error(expr,
+                    "Can't assign or delete a non-expression")
             name = name.expr
 
         if isinstance(name, (ast.Tuple, ast.List)):
@@ -547,9 +566,8 @@ def _storeize(self, expr, name, func=None):
             new_name = ast.Starred(
                 value=self._storeize(expr, name.value, func))
         else:
-            raise HyTypeError(expr,
-                              "Can't assign or delete a %s" %
-                              type(expr).__name__)
+            raise self._syntax_error(expr,
+                "Can't assign or delete a %s" % type(expr).__name__)
 
         new_name.ctx = func()
         ast.copy_location(new_name, name)
@@ -575,9 +593,8 @@ def _render_quoted_form(self, form, level):
             op = unmangle(ast_str(form[0]))
         if level == 0 and op in ("unquote", "unquote-splice"):
             if len(form) != 2:
-                raise HyTypeError(form,
-                                  ("`%s' needs 1 argument, got %s" %
-                                   op, len(form) - 1))
+                raise HyTypeError("`%s' needs 1 argument, got %s" % op, len(form) - 1,
+                                  self.filename, form, self.source)
             return set(), form[1], op == "unquote-splice"
         elif op == "quasiquote":
             level += 1
@@ -629,7 +646,8 @@ def compile_quote(self, expr, root, arg):
     @special("unpack-iterable", [FORM])
     def compile_unpack_iterable(self, expr, root, arg):
         if not PY3:
-            raise HyTypeError(expr, "`unpack-iterable` isn't allowed here")
+            raise self._syntax_error(expr,
+                "`unpack-iterable` isn't allowed here")
         ret = self.compile(arg)
         ret += asty.Starred(expr, value=ret.force_expr, ctx=ast.Load())
         return ret
@@ -659,7 +677,8 @@ def compile_raise_expression(self, expr, root, exc, cause):
 
         if cause is not None:
             if not PY3:
-                raise HyTypeError(expr, "raise from only supported in python 3")
+                raise self._syntax_error(expr,
+                    "raise from only supported in python 3")
             cause = self.compile(cause)
             ret += cause
             cause = cause.force_expr
@@ -706,13 +725,11 @@ def compile_try_expression(self, expr, root, body, catchers, orelse, finalbody):
 
         # Using (else) without (except) is verboten!
         if orelse and not handlers:
-            raise HyTypeError(
-                expr,
+            raise self._syntax_error(expr,
                 "`try' cannot have `else' without `except'")
         # Likewise a bare (try) or (try BODY).
         if not (handlers or finalbody):
-            raise HyTypeError(
-                expr,
+            raise self._syntax_error(expr,
                 "`try' must have an `except' or `finally' clause")
 
         returnable = Result(
@@ -963,7 +980,8 @@ def c(e):
     def compile_decorate_expression(self, expr, name, args):
         decs, fn = args[:-1], self.compile(args[-1])
         if not fn.stmts or not isinstance(fn.stmts[-1], _decoratables):
-            raise HyTypeError(args[-1], "Decorated a non-function")
+            raise self._syntax_error(args[-1],
+                "Decorated a non-function")
         decs, ret, _ = self._compile_collect(decs)
         fn.stmts[-1].decorator_list = decs + fn.stmts[-1].decorator_list
         return ret + fn
@@ -1194,8 +1212,8 @@ def compile_import_or_require(self, expr, root, entries):
                 if (HySymbol('*'), None) in kids:
                     if len(kids) != 1:
                         star = kids[kids.index((HySymbol('*'), None))][0]
-                        raise HyTypeError(star, "* in an import name list "
-                                                "must be on its own")
+                        raise self._syntax_error(star,
+                            "* in an import name list must be on its own")
                 else:
                     assignments = [(k, v or k) for k, v in kids]
 
@@ -1390,15 +1408,15 @@ def _compile_assign(self, name, result):
         if str_name in (["None"] + (["True", "False"] if PY3 else [])):
             # Python 2 allows assigning to True and False, although
             # this is rarely wise.
-            raise HyTypeError(name,
-                              "Can't assign to `%s'" % str_name)
+            raise self._syntax_error(name,
+                "Can't assign to `%s'" % str_name)
 
         result = self.compile(result)
         ld_name = self.compile(name)
 
         if isinstance(ld_name.expr, ast.Call):
-            raise HyTypeError(name,
-                              "Can't assign to a callable: `%s'" % str_name)
+            raise self._syntax_error(name,
+                "Can't assign to a callable: `%s'" % str_name)
 
         if (result.temp_variables
                 and isinstance(name, HySymbol)
@@ -1474,7 +1492,8 @@ def compile_function_def(self, expr, root, params, body):
         mandatory, optional, rest, kwonly, kwargs = params
         optional, defaults, ret = self._parse_optional_args(optional)
         if kwonly is not None and not PY3:
-            raise HyTypeError(params, "&kwonly parameters require Python 3")
+            raise self._syntax_error(params,
+                "&kwonly parameters require Python 3")
         kwonly, kw_defaults, ret2 = self._parse_optional_args(kwonly, True)
         ret += ret2
         main_args = mandatory + optional
@@ -1612,7 +1631,29 @@ def compile_dispatch_tag_macro(self, expr, root, tag, arg):
     def compile_eval_and_compile(self, expr, root, body):
         new_expr = HyExpression([HySymbol("do").replace(expr[0])]).replace(expr)
 
-        hy_eval(new_expr + body, self.module.__dict__, self.module)
+        try:
+            hy_eval(new_expr + body,
+                    self.module.__dict__,
+                    self.module,
+                    filename=self.filename,
+                    source=self.source)
+        except HyInternalError:
+            # Unexpected "meta" compilation errors need to be treated
+            # like normal (unexpected) compilation errors at this level
+            # (or the compilation level preceding this one).
+            raise
+        except Exception as e:
+            # These could be expected Hy language errors (e.g. syntax errors)
+            # or regular Python runtime errors that do not signify errors in
+            # the compilation *process* (although compilation did technically
+            # fail).
+            # We wrap these exceptions and pass them through.
+            reraise(HyEvalError,
+                    HyEvalError(str(e),
+                                self.filename,
+                                body,
+                                self.source),
+                    sys.exc_info()[2])
 
         return (self._compile_branch(body)
                 if ast_str(root) == "eval_and_compile"
@@ -1627,8 +1668,8 @@ def compile_expression(self, expr):
             return self.compile(expr)
 
         if not expr:
-            raise HyTypeError(
-                expr, "empty expressions are not allowed at top level")
+            raise self._syntax_error(expr,
+                "empty expressions are not allowed at top level")
 
         args = list(expr)
         root = args.pop(0)
@@ -1646,8 +1687,7 @@ def compile_expression(self, expr):
                     sroot in (mangle(","), mangle(".")) or
                     not any(is_unpack("iterable", x) for x in args)):
                 if sroot in _bad_roots:
-                    raise HyTypeError(
-                        expr,
+                    raise self._syntax_error(expr,
                         "The special form '{}' is not allowed here".format(root))
                 # `sroot` is a special operator. Get the build method and
                 # pattern-match the arguments.
@@ -1655,11 +1695,10 @@ def compile_expression(self, expr):
                 try:
                     parse_tree = pattern.parse(args)
                 except NoParseError as e:
-                    raise HyTypeError(
+                    raise self._syntax_error(
                         expr[min(e.state.pos + 1, len(expr) - 1)],
                         "parse error for special form '{}': {}".format(
-                            root,
-                            e.msg.replace("<EOF>", "end of form")))
+                            root, e.msg.replace("<EOF>", "end of form")))
                 return Result() + build_method(
                     self, expr, unmangle(sroot), *parse_tree)
 
@@ -1681,13 +1720,13 @@ def compile_expression(self, expr):
                         FORM +
                         many(FORM)).parse(args)
                 except NoParseError:
-                    raise HyTypeError(
-                        expr, "attribute access requires object")
+                    raise self._syntax_error(expr,
+                        "attribute access requires object")
                 # Reconstruct `args` to exclude `obj`.
                 args = [x for p in kws for x in p] + list(rest)
                 if is_unpack("iterable", obj):
-                    raise HyTypeError(
-                        obj, "can't call a method on an unpacking form")
+                    raise self._syntax_error(obj,
+                        "can't call a method on an unpacking form")
                 func = self.compile(HyExpression(
                     [HySymbol(".").replace(root), obj] +
                     attrs))
@@ -1725,16 +1764,12 @@ def compile_symbol(self, symbol):
             glob, local = symbol.rsplit(".", 1)
 
             if not glob:
-                raise HyTypeError(symbol, 'cannot access attribute on '
-                                          'anything other than a name '
-                                          '(in order to get attributes of '
-                                          'expressions, use '
-                                          '`(. <expression> {attr})` or '
-                                          '`(.{attr} <expression>)`)'.format(
-                                              attr=local))
+                raise self._syntax_error(symbol,
+                    'cannot access attribute on anything other than a name (in order to get attributes of expressions, use `(. <expression> {attr})` or `(.{attr} <expression>)`)'.format(attr=local))
 
             if not local:
-                raise HyTypeError(symbol, 'cannot access empty attribute')
+                raise self._syntax_error(symbol,
+                    'cannot access empty attribute')
 
             glob = HySymbol(glob).replace(symbol)
             ret = self.compile_symbol(glob)
@@ -1802,8 +1837,13 @@ def get_compiler_module(module=None, compiler=None, calling_frame=False):
 
 
 def hy_eval(hytree, locals=None, module=None, ast_callback=None,
-            compiler=None):
+            compiler=None, filename=None, source=None):
     """Evaluates a quoted expression and returns the value.
+
+    If you're evaluating hand-crafted AST trees, make sure the line numbers
+    are set properly.  Try `fix_missing_locations` and related functions in the
+    Python `ast` library.
+
     Examples
     --------
        => (eval '(print "Hello World"))
@@ -1816,8 +1856,8 @@ def hy_eval(hytree, locals=None, module=None, ast_callback=None,
 
     Parameters
     ----------
-    hytree: a Hy expression tree
-        Source code to parse.
+    hytree: HyObject
+        The Hy AST object to evaluate.
 
     locals: dict, optional
         Local environment in which to evaluate the Hy tree.  Defaults to the
@@ -1839,6 +1879,19 @@ def hy_eval(hytree, locals=None, module=None, ast_callback=None,
         An existing Hy compiler to use for compilation.  Also serves as
         the `module` value when given.
 
+    filename: str, optional
+        The filename corresponding to the source for `tree`.  This will be
+        overridden by the `filename` field of `tree`, if any; otherwise, it
+        defaults to "<string>".  When `compiler` is given, its `filename` field
+        value is always used.
+
+    source: str, optional
+        A string containing the source code for `tree`.  This will be
+        overridden by the `source` field of `tree`, if any; otherwise,
+        if `None`, an attempt will be made to obtain it from the module given by
+        `module`.  When `compiler` is given, its `source` field value is always
+        used.
+
     Returns
     -------
     out : Result of evaluating the Hy compiled tree.
@@ -1853,36 +1906,53 @@ def hy_eval(hytree, locals=None, module=None, ast_callback=None,
     if not isinstance(locals, dict):
         raise TypeError("Locals must be a dictionary")
 
-    _ast, expr = hy_compile(hytree, module=module, get_expr=True,
-                            compiler=compiler)
-
-    # Spoof the positions in the generated ast...
-    for node in ast.walk(_ast):
-        node.lineno = 1
-        node.col_offset = 1
+    # Does the Hy AST object come with its own information?
+    filename = getattr(hytree, 'filename', filename) or '<string>'
+    source = getattr(hytree, 'source', source)
 
-    for node in ast.walk(expr):
-        node.lineno = 1
-        node.col_offset = 1
+    _ast, expr = hy_compile(hytree, module, get_expr=True,
+                            compiler=compiler, filename=filename,
+                            source=source)
 
     if ast_callback:
         ast_callback(_ast, expr)
 
-    globals = module.__dict__
-
     # Two-step eval: eval() the body of the exec call
-    eval(ast_compile(_ast, "<eval_body>", "exec"), globals, locals)
+    eval(ast_compile(_ast, filename, "exec"),
+         module.__dict__, locals)
 
     # Then eval the expression context and return that
-    return eval(ast_compile(expr, "<eval>", "eval"), globals, locals)
+    return eval(ast_compile(expr, filename, "eval"),
+                module.__dict__, locals)
 
 
-def hy_compile(tree, module=None, root=ast.Module, get_expr=False,
-               compiler=None):
-    """Compile a Hy tree into a Python AST tree.
+def _module_file_source(module_name, filename, source):
+    """Try to obtain missing filename and source information from a module name
+    without actually loading the module.
+    """
+    if filename is None or source is None:
+        mod_loader = pkgutil.get_loader(module_name)
+        if mod_loader:
+            if filename is None:
+                filename = mod_loader.get_filename(module_name)
+            if source is None:
+                source = mod_loader.get_source(module_name)
+
+    # We need a non-None filename.
+    filename = filename or '<string>'
+
+    return filename, source
+
+
+def hy_compile(tree, module, root=ast.Module, get_expr=False,
+               compiler=None, filename=None, source=None):
+    """Compile a HyObject tree into a Python AST Module.
 
     Parameters
     ----------
+    tree: HyObject
+        The Hy AST object to compile.
+
     module: str or types.ModuleType, optional
         Module, or name of the module, in which the Hy tree is evaluated.
         The module associated with `compiler` takes priority over this value.
@@ -1897,18 +1967,43 @@ def hy_compile(tree, module=None, root=ast.Module, get_expr=False,
         An existing Hy compiler to use for compilation.  Also serves as
         the `module` value when given.
 
+    filename: str, optional
+        The filename corresponding to the source for `tree`.  This will be
+        overridden by the `filename` field of `tree`, if any; otherwise, it
+        defaults to "<string>".  When `compiler` is given, its `filename` field
+        value is always used.
+
+    source: str, optional
+        A string containing the source code for `tree`.  This will be
+        overridden by the `source` field of `tree`, if any; otherwise,
+        if `None`, an attempt will be made to obtain it from the module given by
+        `module`.  When `compiler` is given, its `source` field value is always
+        used.
+
     Returns
     -------
     out : A Python AST tree
     """
     module = get_compiler_module(module, compiler, False)
 
+    if isinstance(module, string_types):
+        if module.startswith('<') and module.endswith('>'):
+            module = types.ModuleType(module)
+        else:
+            module = importlib.import_module(ast_str(module, piecewise=True))
+
+    if not inspect.ismodule(module):
+        raise TypeError('Invalid module type: {}'.format(type(module)))
+
+    filename = getattr(tree, 'filename', filename)
+    source = getattr(tree, 'source', source)
+
     tree = wrap_value(tree)
     if not isinstance(tree, HyObject):
-        raise HyCompileError("`tree` must be a HyObject or capable of "
-                             "being promoted to one")
+        raise TypeError("`tree` must be a HyObject or capable of "
+                        "being promoted to one")
 
-    compiler = compiler or HyASTCompiler(module)
+    compiler = compiler or HyASTCompiler(module, filename=filename, source=source)
     result = compiler.compile(tree)
     expr = result.force_expr
 
diff --git a/hy/core/bootstrap.hy b/hy/core/bootstrap.hy
index fa613a601..0d3d737c5 100644
--- a/hy/core/bootstrap.hy
+++ b/hy/core/bootstrap.hy
@@ -14,15 +14,14 @@
      (if* (not (isinstance macro-name hy.models.HySymbol))
           (raise
             (hy.errors.HyTypeError
-              macro-name
               (% "received a `%s' instead of a symbol for macro name"
-                 (. (type name)
-                    __name__)))))
+                 (. (type name) __name__))
+              None --file-- None)))
      (for [kw '[&kwonly &kwargs]]
        (if* (in kw lambda-list)
-            (raise (hy.errors.HyTypeError macro-name
-                                          (% "macros cannot use %s"
-                                             kw)))))
+            (raise (hy.errors.HyTypeError (% "macros cannot use %s"
+                                             kw)
+                                          macro-name --file-- None))))
      ;; this looks familiar...
      `(eval-and-compile
         (import hy)
@@ -45,12 +44,12 @@
   (if (and (not (isinstance tag-name hy.models.HySymbol))
            (not (isinstance tag-name hy.models.HyString)))
       (raise (hy.errors.HyTypeError
-               tag-name
                (% "received a `%s' instead of a symbol for tag macro name"
-                  (. (type tag-name) __name__)))))
+                  (. (type tag-name) --name--))
+               tag-name --file-- None)))
   (if (or (= tag-name ":")
           (= tag-name "&"))
-      (raise (NameError (% "%s can't be used as a tag macro name" tag-name))))
+      (raise (hy.errors.HyNameError (% "%s can't be used as a tag macro name" tag-name))))
   (setv tag-name (.replace (hy.models.HyString tag-name)
                            tag-name))
   `(eval-and-compile
@@ -58,9 +57,8 @@
      ((hy.macros.tag ~tag-name)
       (fn ~lambda-list ~@body))))
 
-(defmacro macro-error [location reason]
-  "Error out properly within a macro at `location` giving `reason`."
-  `(raise (hy.errors.HyMacroExpansionError ~location ~reason)))
+(defmacro macro-error [expression reason &optional [filename '--name--]]
+  `(raise (hy.errors.HyMacroExpansionError ~reason ~filename ~expression None)))
 
 (defmacro defn [name lambda-list &rest body]
   "Define `name` as a function with `lambda-list` signature and body `body`."
diff --git a/hy/errors.py b/hy/errors.py
index 7d36ab2e1..0b7619e3d 100644
--- a/hy/errors.py
+++ b/hy/errors.py
@@ -2,103 +2,307 @@
 # Copyright 2019 the authors.
 # This file is part of Hy, which is free software licensed under the Expat
 # license. See the LICENSE.
-
+import os
+import re
+import sys
 import traceback
+import pkgutil
+
+from functools import reduce
+from contextlib import contextmanager
+from hy import _initialize_env_var
 
 from clint.textui import colored
 
+_hy_filter_internal_errors = _initialize_env_var('HY_FILTER_INTERNAL_ERRORS',
+                                                 True)
+_hy_colored_errors = _initialize_env_var('HY_COLORED_ERRORS', False)
+
 
 class HyError(Exception):
+    pass
+
+
+class HyInternalError(HyError):
+    """Unexpected errors occurring during compilation or parsing of Hy code.
+
+    Errors sub-classing this are not intended to be user-facing, and will,
+    hopefully, never be seen by users!
     """
-    Generic Hy error. All internal Exceptions will be subclassed from this
-    Exception.
+
+
+class HyLanguageError(HyError):
+    """Errors caused by invalid use of the Hy language.
+
+    This, and any errors inheriting from this, are user-facing.
     """
-    pass
 
+    def __init__(self, message, expression=None, filename=None, source=None,
+                 lineno=1, colno=1):
+        """
+        Parameters
+        ----------
+        message: str
+            The message to display for this error.
+        expression: HyObject, optional
+            The Hy expression generating this error.
+        filename: str, optional
+            The filename for the source code generating this error.
+            Expression-provided information will take precedence of this value.
+        source: str, optional
+            The actual source code generating this error.  Expression-provided
+            information will take precedence of this value.
+        lineno: int, optional
+            The line number of the error.  Expression-provided information will
+            take precedence of this value.
+        colno: int, optional
+            The column number of the error.  Expression-provided information
+            will take precedence of this value.
+        """
+        self.msg = message
+        self.compute_lineinfo(expression, filename, source, lineno, colno)
+
+        if isinstance(self, SyntaxError):
+            syntax_error_args = (self.filename, self.lineno, self.offset,
+                                 self.text)
+            super(HyLanguageError, self).__init__(message, syntax_error_args)
+        else:
+            super(HyLanguageError, self).__init__(message)
+
+    def compute_lineinfo(self, expression, filename, source, lineno, colno):
+
+        # NOTE: We use `SyntaxError`'s field names (i.e. `text`, `offset`,
+        # `msg`) for compatibility and print-outs.
+        self.text = getattr(expression, 'source', source)
+        self.filename = getattr(expression, 'filename', filename)
+
+        if self.text:
+            lines = self.text.splitlines()
+
+            self.lineno = getattr(expression, 'start_line', lineno)
+            self.offset = getattr(expression, 'start_column', colno)
+            end_column = getattr(expression, 'end_column',
+                                 len(lines[self.lineno-1]))
+            end_line = getattr(expression, 'end_line', self.lineno)
 
-class HyCompileError(HyError):
-    def __init__(self, exception, traceback=None):
-        self.exception = exception
-        self.traceback = traceback
+            # Trim the source down to the essentials.
+            self.text = '\n'.join(lines[self.lineno-1:end_line])
+
+            if end_column:
+                if self.lineno == end_line:
+                    self.arrow_offset = end_column
+                else:
+                    self.arrow_offset = len(self.text[0])
+
+                self.arrow_offset -= self.offset
+            else:
+                self.arrow_offset = None
+        else:
+            # We could attempt to extract the source given a filename, but we
+            # don't.
+            self.lineno = lineno
+            self.offset = colno
+            self.arrow_offset = None
 
     def __str__(self):
-        if isinstance(self.exception, HyTypeError):
-            return str(self.exception)
-        if self.traceback:
-            tb = "".join(traceback.format_tb(self.traceback)).strip()
+        """Provide an exception message that includes SyntaxError-like source
+        line information when available.
+        """
+        global _hy_colored_errors
+
+        # Syntax errors are special and annotate the traceback (instead of what
+        # we would do in the message that follows the traceback).
+        if isinstance(self, SyntaxError):
+            return super(HyLanguageError, self).__str__()
+
+        # When there isn't extra source information, use the normal message.
+        if not isinstance(self, SyntaxError) and not self.text:
+            return super(HyLanguageError, self).__str__()
+
+        # Re-purpose Python's builtin syntax error formatting.
+        output = traceback.format_exception_only(
+            SyntaxError,
+            SyntaxError(self.msg, (self.filename, self.lineno, self.offset,
+                                   self.text)))
+
+        arrow_idx, _ = next(((i, x) for i, x in enumerate(output)
+                             if x.strip() == '^'),
+                            (None, None))
+        if arrow_idx:
+            msg_idx = arrow_idx + 1
         else:
-            tb = "No traceback available. 😟"
-        return("Internal Compiler Bug 😱\n⤷ %s: %s\nCompilation traceback:\n%s"
-               % (self.exception.__class__.__name__,
-                  self.exception, tb))
+            msg_idx, _ = next((i, x) for i, x in enumerate(output)
+                              if x.startswith('SyntaxError: '))
 
+        # Get rid of erroneous error-type label.
+        output[msg_idx] = re.sub('^SyntaxError: ', '', output[msg_idx])
 
-class HyTypeError(TypeError):
-    def __init__(self, expression, message):
-        super(HyTypeError, self).__init__(message)
-        self.expression = expression
-        self.message = message
-        self.source = None
-        self.filename = None
+        # Extend the text arrow, when given enough source info.
+        if arrow_idx and self.arrow_offset:
+            output[arrow_idx] = '{}{}^\n'.format(output[arrow_idx].rstrip('\n'),
+                                                 '-' * (self.arrow_offset - 1))
 
-    def __str__(self):
+        if _hy_colored_errors:
+            from clint.textui import colored
+            output[msg_idx:] = [colored.yellow(o) for o in output[msg_idx:]]
+            if arrow_idx:
+                output[arrow_idx] = colored.green(output[arrow_idx])
+            for idx, line in enumerate(output[::msg_idx]):
+                if line.strip().startswith(
+                        'File "{}", line'.format(self.filename)):
+                    output[idx] = colored.red(line)
 
-        result = ""
+        # This resulting string will come after a "<class-name>:" prompt, so
+        # put it down a line.
+        output.insert(0, '\n')
 
-        if all(getattr(self.expression, x, None) is not None
-               for x in ("start_line", "start_column", "end_column")):
+        # Avoid "...expected str instance, ColoredString found"
+        return reduce(lambda x, y: x + y, output)
 
-            line = self.expression.start_line
-            start = self.expression.start_column
-            end = self.expression.end_column
 
-            source = []
-            if self.source is not None:
-                source = self.source.split("\n")[line-1:self.expression.end_line]
+class HyCompileError(HyInternalError):
+    """Unexpected errors occurring within the compiler."""
 
-                if line == self.expression.end_line:
-                    length = end - start
-                else:
-                    length = len(source[0]) - start
-
-            result += '  File "%s", line %d, column %d\n\n' % (self.filename,
-                                                               line,
-                                                               start)
-
-            if len(source) == 1:
-                result += '  %s\n' % colored.red(source[0])
-                result += '  %s%s\n' % (' '*(start-1),
-                                        colored.green('^' + '-'*(length-1) + '^'))
-            if len(source) > 1:
-                result += '  %s\n' % colored.red(source[0])
-                result += '  %s%s\n' % (' '*(start-1),
-                                        colored.green('^' + '-'*length))
-                if len(source) > 2:  # write the middle lines
-                    for line in source[1:-1]:
-                        result += '  %s\n' % colored.red("".join(line))
-                        result += '  %s\n' % colored.green("-"*len(line))
-
-                # write the last line
-                result += '  %s\n' % colored.red("".join(source[-1]))
-                result += '  %s\n' % colored.green('-'*(end-1) + '^')
 
+class HyTypeError(HyLanguageError, TypeError):
+    """TypeError occurring during the normal use of Hy."""
+
+
+class HyNameError(HyLanguageError, NameError):
+    """NameError occurring during the normal use of Hy."""
+
+
+class HyRequireError(HyLanguageError):
+    """Errors arising during the use of `require`
+
+    This, and any errors inheriting from this, are user-facing.
+    """
+
+
+class HyMacroExpansionError(HyLanguageError):
+    """Errors caused by invalid use of Hy macros.
+
+    This, and any errors inheriting from this, are user-facing.
+    """
+
+
+class HyEvalError(HyLanguageError):
+    """Errors occurring during code evaluation at compile-time.
+
+    These errors distinguish unexpected errors within the compilation process
+    (i.e. `HyInternalError`s) from unrelated errors in user code evaluated by
+    the compiler (e.g. in `eval-and-compile`).
+
+    This, and any errors inheriting from this, are user-facing.
+    """
+
+
+class HyIOError(HyInternalError, IOError):
+    """ Subclass used to distinguish between IOErrors raised by Hy itself as
+    opposed to Hy programs.
+    """
+
+
+class HySyntaxError(HyLanguageError, SyntaxError):
+    """Error during the Lexing of a Hython expression."""
+
+
+class HyWrapperError(HyError, TypeError):
+    """Errors caused by language model object wrapping.
+
+    These can be caused by improper user-level use of a macro, so they're
+    not really "internal".  If they arise due to anything else, they're an
+    internal/compiler problem, though.
+    """
+
+
+def _module_filter_name(module_name):
+    try:
+        compiler_loader = pkgutil.get_loader(module_name)
+        if not compiler_loader:
+            return None
+
+        filename = compiler_loader.get_filename(module_name)
+        if not filename:
+            return None
+
+        if compiler_loader.is_package(module_name):
+            # Use the package directory (e.g. instead of `.../__init__.py`) so
+            # that we can filter all modules in a package.
+            return os.path.dirname(filename)
         else:
-            result += '  File "%s", unknown location\n' % self.filename
+            # Normalize filename endings, because tracebacks will use `pyc` when
+            # the loader says `py`.
+            return filename.replace('.pyc', '.py')
+    except Exception:
+        return None
 
-        result += colored.yellow("%s: %s\n\n" %
-                                 (self.__class__.__name__,
-                                  self.message))
 
-        return result
+_tb_hidden_modules = {m for m in map(_module_filter_name,
+                                     ['hy.compiler', 'hy.lex',
+                                      'hy.cmdline', 'hy.lex.parser',
+                                      'hy.importer', 'hy._compat',
+                                      'hy.macros', 'hy.models',
+                                      'rply'])
+                      if m is not None}
 
 
-class HyMacroExpansionError(HyTypeError):
-    pass
+def hy_exc_filter(exc_type, exc_value, exc_traceback):
+    """Produce exceptions print-outs with all frames originating from the
+    modules in `_tb_hidden_modules` filtered out.
 
+    The frames are actually filtered by each module's filename and only when a
+    subclass of `HyLanguageError` is emitted.
 
-class HyIOError(HyError, IOError):
+    This does not remove the frames from the actual tracebacks, so debugging
+    will show everything.
     """
-    Trivial subclass of IOError and HyError, to distinguish between
-    IOErrors raised by Hy itself as opposed to Hy programs.
+    # frame = (filename, line number, function name*, text)
+    new_tb = []
+    for frame in traceback.extract_tb(exc_traceback):
+        if not (frame[0].replace('.pyc', '.py') in _tb_hidden_modules or
+                os.path.dirname(frame[0]) in _tb_hidden_modules):
+            new_tb += [frame]
+
+    lines = traceback.format_list(new_tb)
+
+    lines.insert(0, "Traceback (most recent call last):\n")
+
+    lines.extend(traceback.format_exception_only(exc_type, exc_value))
+    output = ''.join(lines)
+
+    return output
+
+
+def hy_exc_handler(exc_type, exc_value, exc_traceback):
+    """A `sys.excepthook` handler that uses `hy_exc_filter` to
+    remove internal Hy frames from a traceback print-out.
     """
-    pass
+    if os.environ.get('HY_DEBUG', False):
+        return sys.__excepthook__(exc_type, exc_value, exc_traceback)
+
+    try:
+        output = hy_exc_filter(exc_type, exc_value, exc_traceback)
+        sys.stderr.write(output)
+        sys.stderr.flush()
+    except Exception:
+        sys.__excepthook__(exc_type, exc_value, exc_traceback)
+
+
+@contextmanager
+def filtered_hy_exceptions():
+    """Temporarily apply a `sys.excepthook` that filters Hy internal frames
+    from tracebacks.
+
+    Filtering can be controlled by the variable
+    `hy.errors._hy_filter_internal_errors` and environment variable
+    `HY_FILTER_INTERNAL_ERRORS`.
+    """
+    global _hy_filter_internal_errors
+    if _hy_filter_internal_errors:
+        current_hook = sys.excepthook
+        sys.excepthook = hy_exc_handler
+        yield
+        sys.excepthook = current_hook
+    else:
+        yield
diff --git a/hy/importer.py b/hy/importer.py
index e3cc50d24..0e5949833 100644
--- a/hy/importer.py
+++ b/hy/importer.py
@@ -17,10 +17,8 @@
 from functools import partial
 from contextlib import contextmanager
 
-from hy.errors import HyTypeError
 from hy.compiler import hy_compile, hy_ast_compile_flags
 from hy.lex import hy_parse
-from hy.lex.exceptions import LexException
 from hy._compat import PY3
 
 
@@ -153,15 +151,9 @@ def _could_be_hy_src(filename):
     def _hy_source_to_code(self, data, path, _optimize=-1):
         if _could_be_hy_src(path):
             source = data.decode("utf-8")
-            try:
-                hy_tree = hy_parse(source)
-                with loader_module_obj(self) as module:
-                    data = hy_compile(hy_tree, module)
-            except (HyTypeError, LexException) as e:
-                if e.source is None:
-                    e.source = source
-                    e.filename = path
-                raise
+            hy_tree = hy_parse(source, filename=path)
+            with loader_module_obj(self) as module:
+                data = hy_compile(hy_tree, module)
 
         return _py_source_to_code(self, data, path, _optimize=_optimize)
 
@@ -287,19 +279,15 @@ def byte_compile_hy(self, fullname=None):
             fullname = self._fix_name(fullname)
             if fullname is None:
                 fullname = self.fullname
-            try:
-                hy_source = self.get_source(fullname)
-                hy_tree = hy_parse(hy_source)
-                with loader_module_obj(self) as module:
-                    hy_ast = hy_compile(hy_tree, module)
-
-                code = compile(hy_ast, self.filename, 'exec',
-                               hy_ast_compile_flags)
-            except (HyTypeError, LexException) as e:
-                if e.source is None:
-                    e.source = hy_source
-                    e.filename = self.filename
-                raise
+
+            hy_source = self.get_source(fullname)
+            hy_tree = hy_parse(hy_source, filename=self.filename)
+
+            with loader_module_obj(self) as module:
+                hy_ast = hy_compile(hy_tree, module)
+
+            code = compile(hy_ast, self.filename, 'exec',
+                           hy_ast_compile_flags)
 
             if not sys.dont_write_bytecode:
                 try:
@@ -453,7 +441,7 @@ def hyc_compile(file_or_code, cfile=None, dfile=None, doraise=False,
             try:
                 flags = None
                 if _could_be_hy_src(filename):
-                    hy_tree = hy_parse(source_str)
+                    hy_tree = hy_parse(source_str, filename=filename)
 
                     if module is None:
                         module = inspect.getmodule(inspect.stack()[1][0])
@@ -465,9 +453,6 @@ def hyc_compile(file_or_code, cfile=None, dfile=None, doraise=False,
 
                 codeobject = compile(source, dfile or filename, 'exec', flags)
             except Exception as err:
-                if isinstance(err, (HyTypeError, LexException)) and err.source is None:
-                    err.source = source_str
-                    err.filename = filename
 
                 py_exc = py_compile.PyCompileError(err.__class__, err,
                                                    dfile or filename)
diff --git a/hy/lex/__init__.py b/hy/lex/__init__.py
index bf82cf945..eb3ac41f4 100644
--- a/hy/lex/__init__.py
+++ b/hy/lex/__init__.py
@@ -18,7 +18,7 @@
     from StringIO import StringIO
 
 
-def hy_parse(source):
+def hy_parse(source, filename='<string>'):
     """Parse a Hy source string.
 
     Parameters
@@ -26,31 +26,52 @@ def hy_parse(source):
     source: string
         Source code to parse.
 
+    filename: string, optional
+        File name corresponding to source.  Defaults to "<string>".
+
     Returns
     -------
-    out : instance of `types.CodeType`
+    out : HyExpression
     """
-    source = re.sub(r'\A#!.*', '', source)
-    return HyExpression([HySymbol("do")] + tokenize(source + "\n"))
+    _source = re.sub(r'\A#!.*', '', source)
+    res = HyExpression([HySymbol("do")] +
+                       tokenize(_source + "\n",
+                                filename=filename))
+    res.source = source
+    res.filename = filename
+    return res
 
 
-def tokenize(buf):
-    """
-    Tokenize a Lisp file or string buffer into internal Hy objects.
+class ParserState(object):
+    def __init__(self, source, filename):
+        self.source = source
+        self.filename = filename
+
+
+def tokenize(source, filename=None):
+    """ Tokenize a Lisp file or string buffer into internal Hy objects.
+
+    Parameters
+    ----------
+    source: str
+        The source to tokenize.
+    filename: str, optional
+        The filename corresponding to `source`.
     """
     from hy.lex.lexer import lexer
     from hy.lex.parser import parser
     from rply.errors import LexingError
     try:
-        return parser.parse(lexer.lex(buf))
+        return parser.parse(lexer.lex(source),
+                            state=ParserState(source, filename))
     except LexingError as e:
         pos = e.getsourcepos()
         raise LexException("Could not identify the next token.",
-                           pos.lineno, pos.colno, buf)
+                           None, filename, source,
+                           max(pos.lineno, 1),
+                           max(pos.colno, 1))
     except LexException as e:
-        if e.source is None:
-            e.source = buf
-        raise
+        raise e
 
 
 mangle_delim = 'X'
diff --git a/hy/lex/exceptions.py b/hy/lex/exceptions.py
index 573a8e857..449119a21 100644
--- a/hy/lex/exceptions.py
+++ b/hy/lex/exceptions.py
@@ -1,49 +1,39 @@
 # Copyright 2019 the authors.
 # This file is part of Hy, which is free software licensed under the Expat
 # license. See the LICENSE.
-
-from hy.errors import HyError
-
-
-class LexException(HyError):
-    """Error during the Lexing of a Hython expression."""
-    def __init__(self, message, lineno, colno, source=None):
-        super(LexException, self).__init__(message)
-        self.message = message
-        self.lineno = lineno
-        self.colno = colno
-        self.source = source
-        self.filename = '<stdin>'
-
-    def __str__(self):
-        from hy.errors import colored
-
-        line = self.lineno
-        start = self.colno
-
-        result = ""
-
-        source = self.source.split("\n")
-
-        if line > 0 and start > 0:
-            result += '  File "%s", line %d, column %d\n\n' % (self.filename,
-                                                               line,
-                                                               start)
-
-            if len(self.source) > 0:
-                source_line = source[line-1]
-            else:
-                source_line = ""
-
-            result += '  %s\n' % colored.red(source_line)
-            result += '  %s%s\n' % (' '*(start-1), colored.green('^'))
-
-        result += colored.yellow("LexException: %s\n\n" % self.message)
-
-        return result
+from hy.errors import HySyntaxError
+
+
+class LexException(HySyntaxError):
+
+    @classmethod
+    def from_lexer(cls, message, state, token):
+        lineno = None
+        colno = None
+        source = state.source
+        source_pos = token.getsourcepos()
+
+        if source_pos:
+            lineno = source_pos.lineno
+            colno = source_pos.colno
+        elif source:
+            # Use the end of the last line of source for `PrematureEndOfInput`.
+            # We get rid of empty lines and spaces so that the error matches
+            # with the last piece of visible code.
+            lines = source.rstrip().splitlines()
+            lineno = lineno or len(lines)
+            colno = colno or len(lines[lineno - 1])
+        else:
+            lineno = lineno or 1
+            colno = colno or 1
+
+        return cls(message,
+                   None,
+                   state.filename,
+                   source,
+                   lineno,
+                   colno)
 
 
 class PrematureEndOfInput(LexException):
-    """We got a premature end of input"""
-    def __init__(self, message):
-        super(PrematureEndOfInput, self).__init__(message, -1, -1)
+    pass
diff --git a/hy/lex/parser.py b/hy/lex/parser.py
index c602734a5..c4df2a53d 100755
--- a/hy/lex/parser.py
+++ b/hy/lex/parser.py
@@ -6,7 +6,6 @@
 from __future__ import unicode_literals
 
 from functools import wraps
-import re, unicodedata
 
 from rply import ParserGenerator
 
@@ -22,10 +21,10 @@
 
 def set_boundaries(fun):
     @wraps(fun)
-    def wrapped(p):
+    def wrapped(state, p):
         start = p[0].source_pos
         end = p[-1].source_pos
-        ret = fun(p)
+        ret = fun(state, p)
         ret.start_line = start.lineno
         ret.start_column = start.colno
         if start is not end:
@@ -40,9 +39,9 @@ def wrapped(p):
 
 def set_quote_boundaries(fun):
     @wraps(fun)
-    def wrapped(p):
+    def wrapped(state, p):
         start = p[0].source_pos
-        ret = fun(p)
+        ret = fun(state, p)
         ret.start_line = start.lineno
         ret.start_column = start.colno
         ret.end_line = p[-1].end_line
@@ -52,54 +51,45 @@ def wrapped(p):
 
 
 @pg.production("main : list_contents")
-def main(p):
+def main(state, p):
     return p[0]
 
 
 @pg.production("main : $end")
-def main_empty(p):
+def main_empty(state, p):
     return []
 
 
-def reject_spurious_dots(*items):
-    "Reject the spurious dots from items"
-    for list in items:
-        for tok in list:
-            if tok == "." and type(tok) == HySymbol:
-                raise LexException("Malformed dotted list",
-                                   tok.start_line, tok.start_column)
-
-
 @pg.production("paren : LPAREN list_contents RPAREN")
 @set_boundaries
-def paren(p):
+def paren(state, p):
     return HyExpression(p[1])
 
 
 @pg.production("paren : LPAREN RPAREN")
 @set_boundaries
-def empty_paren(p):
+def empty_paren(state, p):
     return HyExpression([])
 
 
 @pg.production("list_contents : term list_contents")
-def list_contents(p):
+def list_contents(state, p):
     return [p[0]] + p[1]
 
 
 @pg.production("list_contents : term")
-def list_contents_single(p):
+def list_contents_single(state, p):
     return [p[0]]
 
 
 @pg.production("list_contents : DISCARD term discarded_list_contents")
-def list_contents_empty(p):
+def list_contents_empty(state, p):
     return []
 
 
 @pg.production("discarded_list_contents : DISCARD term discarded_list_contents")
 @pg.production("discarded_list_contents :")
-def discarded_list_contents(p):
+def discarded_list_contents(state, p):
     pass
 
 
@@ -109,58 +99,58 @@ def discarded_list_contents(p):
 @pg.production("term : list")
 @pg.production("term : set")
 @pg.production("term : string")
-def term(p):
+def term(state, p):
     return p[0]
 
 
 @pg.production("term : DISCARD term term")
-def term_discard(p):
+def term_discard(state, p):
     return p[2]
 
 
 @pg.production("term : QUOTE term")
 @set_quote_boundaries
-def term_quote(p):
+def term_quote(state, p):
     return HyExpression([HySymbol("quote"), p[1]])
 
 
 @pg.production("term : QUASIQUOTE term")
 @set_quote_boundaries
-def term_quasiquote(p):
+def term_quasiquote(state, p):
     return HyExpression([HySymbol("quasiquote"), p[1]])
 
 
 @pg.production("term : UNQUOTE term")
 @set_quote_boundaries
-def term_unquote(p):
+def term_unquote(state, p):
     return HyExpression([HySymbol("unquote"), p[1]])
 
 
 @pg.production("term : UNQUOTESPLICE term")
 @set_quote_boundaries
-def term_unquote_splice(p):
+def term_unquote_splice(state, p):
     return HyExpression([HySymbol("unquote-splice"), p[1]])
 
 
 @pg.production("term : HASHSTARS term")
 @set_quote_boundaries
-def term_hashstars(p):
+def term_hashstars(state, p):
     n_stars = len(p[0].getstr()[1:])
     if n_stars == 1:
         sym = "unpack-iterable"
     elif n_stars == 2:
         sym = "unpack-mapping"
     else:
-        raise LexException(
+        raise LexException.from_lexer(
             "Too many stars in `#*` construct (if you want to unpack a symbol "
             "beginning with a star, separate it with whitespace)",
-            p[0].source_pos.lineno, p[0].source_pos.colno)
+            state, p[0])
     return HyExpression([HySymbol(sym), p[1]])
 
 
 @pg.production("term : HASHOTHER term")
 @set_quote_boundaries
-def hash_other(p):
+def hash_other(state, p):
     # p == [(Token('HASHOTHER', '#foo'), bar)]
     st = p[0].getstr()[1:]
     str_object = HyString(st)
@@ -170,63 +160,63 @@ def hash_other(p):
 
 @pg.production("set : HLCURLY list_contents RCURLY")
 @set_boundaries
-def t_set(p):
+def t_set(state, p):
     return HySet(p[1])
 
 
 @pg.production("set : HLCURLY RCURLY")
 @set_boundaries
-def empty_set(p):
+def empty_set(state, p):
     return HySet([])
 
 
 @pg.production("dict : LCURLY list_contents RCURLY")
 @set_boundaries
-def t_dict(p):
+def t_dict(state, p):
     return HyDict(p[1])
 
 
 @pg.production("dict : LCURLY RCURLY")
 @set_boundaries
-def empty_dict(p):
+def empty_dict(state, p):
     return HyDict([])
 
 
 @pg.production("list : LBRACKET list_contents RBRACKET")
 @set_boundaries
-def t_list(p):
+def t_list(state, p):
     return HyList(p[1])
 
 
 @pg.production("list : LBRACKET RBRACKET")
 @set_boundaries
-def t_empty_list(p):
+def t_empty_list(state, p):
     return HyList([])
 
 
 @pg.production("string : STRING")
 @set_boundaries
-def t_string(p):
+def t_string(state, p):
     # Replace the single double quotes with triple double quotes to allow
     # embedded newlines.
     try:
         s = eval(p[0].value.replace('"', '"""', 1)[:-1] + '"""')
     except SyntaxError:
-        raise LexException("Can't convert {} to a HyString".format(p[0].value),
-            p[0].source_pos.lineno, p[0].source_pos.colno)
+        raise LexException.from_lexer("Can't convert {} to a HyString".format(p[0].value),
+                                      state, p[0])
     return (HyString if isinstance(s, str_type) else HyBytes)(s)
 
 
 @pg.production("string : PARTIAL_STRING")
-def t_partial_string(p):
+def t_partial_string(state, p):
     # Any unterminated string requires more input
-    raise PrematureEndOfInput("Premature end of input")
+    raise PrematureEndOfInput.from_lexer("Partial string literal", state, p[0])
 
 
 bracket_string_re = next(r.re for r in lexer.rules if r.name == 'BRACKETSTRING')
 @pg.production("string : BRACKETSTRING")
 @set_boundaries
-def t_bracket_string(p):
+def t_bracket_string(state, p):
     m = bracket_string_re.match(p[0].value)
     delim, content = m.groups()
     return HyString(content, brackets=delim)
@@ -234,7 +224,7 @@ def t_bracket_string(p):
 
 @pg.production("identifier : IDENTIFIER")
 @set_boundaries
-def t_identifier(p):
+def t_identifier(state, p):
     obj = p[0].value
 
     val = symbol_like(obj)
@@ -243,11 +233,11 @@ def t_identifier(p):
 
     if "." in obj and symbol_like(obj.split(".", 1)[0]) is not None:
         # E.g., `5.attr` or `:foo.attr`
-        raise LexException(
+        raise LexException.from_lexer(
             'Cannot access attribute on anything other than a name (in '
             'order to get attributes of expressions, use '
             '`(. <expression> <attr>)` or `(.<attr> <expression>)`)',
-            p[0].source_pos.lineno, p[0].source_pos.colno)
+            state, p[0])
 
     return HySymbol(obj)
 
@@ -284,14 +274,15 @@ def symbol_like(obj):
 
 
 @pg.error
-def error_handler(token):
+def error_handler(state, token):
     tokentype = token.gettokentype()
     if tokentype == '$end':
-        raise PrematureEndOfInput("Premature end of input")
+        raise PrematureEndOfInput.from_lexer("Premature end of input", state,
+                                             token)
     else:
-        raise LexException(
-            "Ran into a %s where it wasn't expected." % tokentype,
-            token.source_pos.lineno, token.source_pos.colno)
+        raise LexException.from_lexer(
+            "Ran into a %s where it wasn't expected." % tokentype, state,
+            token)
 
 
 parser = pg.build()
diff --git a/hy/macros.py b/hy/macros.py
index be59200ab..e2cec31ac 100644
--- a/hy/macros.py
+++ b/hy/macros.py
@@ -1,15 +1,19 @@
 # Copyright 2019 the authors.
 # This file is part of Hy, which is free software licensed under the Expat
 # license. See the LICENSE.
+import sys
 import importlib
 import inspect
 import pkgutil
+import traceback
 
-from hy._compat import PY3, string_types
+from contextlib import contextmanager
+
+from hy._compat import PY3, string_types, reraise, rename_function
 from hy.models import replace_hy_obj, HyExpression, HySymbol, wrap_value
 from hy.lex import mangle
-
-from hy.errors import HyTypeError, HyMacroExpansionError
+from hy.errors import (HyLanguageError, HyMacroExpansionError, HyTypeError,
+                       HyRequireError)
 
 try:
     # Check if we have the newer inspect.signature available.
@@ -48,7 +52,7 @@ def macro(name):
     """
     name = mangle(name)
     def _(fn):
-        fn.__name__ = '({})'.format(name)
+        fn = rename_function(fn, name)
         try:
             fn._hy_macro_pass_compiler = has_kwargs(fn)
         except Exception:
@@ -73,7 +77,7 @@ def _(fn):
         if not PY3:
             _name = _name.encode('UTF-8')
 
-        fn.__name__ = _name
+        fn = rename_function(fn, _name)
 
         module = inspect.getmodule(fn)
 
@@ -148,7 +152,6 @@ def require(source_module, target_module, assignments, prefix=""):
     out: boolean
         Whether or not macros and tags were actually transferred.
     """
-
     if target_module is None:
         parent_frame = inspect.stack()[1][0]
         target_namespace = parent_frame.f_globals
@@ -159,7 +162,7 @@ def require(source_module, target_module, assignments, prefix=""):
     elif inspect.ismodule(target_module):
         target_namespace = target_module.__dict__
     else:
-        raise TypeError('`target_module` is not a recognized type: {}'.format(
+        raise HyTypeError('`target_module` is not a recognized type: {}'.format(
             type(target_module)))
 
     # Let's do a quick check to make sure the source module isn't actually
@@ -171,14 +174,17 @@ def require(source_module, target_module, assignments, prefix=""):
         return False
 
     if not inspect.ismodule(source_module):
-        source_module = importlib.import_module(source_module)
+        try:
+            source_module = importlib.import_module(source_module)
+        except ImportError as e:
+            reraise(HyRequireError, HyRequireError(e.args[0]), None)
 
     source_macros = source_module.__dict__.setdefault('__macros__', {})
     source_tags = source_module.__dict__.setdefault('__tags__', {})
 
     if len(source_module.__macros__) + len(source_module.__tags__) == 0:
         if assignments != "ALL":
-            raise ImportError('The module {} has no macros or tags'.format(
+            raise HyRequireError('The module {} has no macros or tags'.format(
                 source_module))
         else:
             return False
@@ -203,7 +209,7 @@ def require(source_module, target_module, assignments, prefix=""):
         elif _name in source_module.__tags__:
             target_tags[alias] = source_tags[_name]
         else:
-            raise ImportError('Could not require name {} from {}'.format(
+            raise HyRequireError('Could not require name {} from {}'.format(
                 _name, source_module))
 
     return True
@@ -237,24 +243,33 @@ def load_macros(module):
                                 if k not in module_tags})
 
 
-def make_empty_fn_copy(fn):
+@contextmanager
+def macro_exceptions(module, macro_tree, compiler=None):
     try:
-        # This might fail if fn has parameters with funny names, like o!n. In
-        # such a case, we return a generic function that ensures the program
-        # can continue running. Unfortunately, the error message that might get
-        # raised later on while expanding a macro might not make sense at all.
-
-        formatted_args = format_args(fn)
-        fn_str = 'lambda {}: None'.format(
-            formatted_args.lstrip('(').rstrip(')'))
-        empty_fn = eval(fn_str)
+        yield
+    except HyLanguageError as e:
+        # These are user-level Hy errors occurring in the macro.
+        # We want to pass them up to the user.
+        reraise(type(e), e, sys.exc_info()[2])
+    except Exception as e:
+
+        if compiler:
+            filename = compiler.filename
+            source = compiler.source
+        else:
+            filename = None
+            source = None
 
-    except Exception:
+        exc_msg = '  '.join(traceback.format_exception_only(
+            sys.exc_info()[0], sys.exc_info()[1]))
 
-        def empty_fn(*args, **kwargs):
-            None
+        msg = "expanding macro {}\n  ".format(str(macro_tree[0]))
+        msg += exc_msg
 
-    return empty_fn
+        reraise(HyMacroExpansionError,
+                HyMacroExpansionError(
+                    msg, macro_tree, filename, source),
+                sys.exc_info()[2])
 
 
 def macroexpand(tree, module, compiler=None, once=False):
@@ -324,28 +339,13 @@ def macroexpand(tree, module, compiler=None, once=False):
                 compiler = HyASTCompiler(module)
             opts['compiler'] = compiler
 
-        try:
-            m_copy = make_empty_fn_copy(m)
-            m_copy(module.__name__, *tree[1:], **opts)
-        except TypeError as e:
-            msg = "expanding `" + str(tree[0]) + "': "
-            msg += str(e).replace("<lambda>()", "", 1).strip()
-            raise HyMacroExpansionError(tree, msg)
-
-        try:
+        with macro_exceptions(module, tree, compiler):
             obj = m(module.__name__, *tree[1:], **opts)
-        except HyTypeError as e:
-            if e.expression is None:
-                e.expression = tree
-            raise
-        except Exception as e:
-            msg = "expanding `" + str(tree[0]) + "': " + repr(e)
-            raise HyMacroExpansionError(tree, msg)
 
-        if isinstance(obj, HyExpression):
-            obj.module = inspect.getmodule(m)
+            if isinstance(obj, HyExpression):
+                obj.module = inspect.getmodule(m)
 
-        tree = replace_hy_obj(obj, tree)
+            tree = replace_hy_obj(obj, tree)
 
         if once:
             break
@@ -375,7 +375,8 @@ def tag_macroexpand(tag, tree, module):
                      None)
 
     if tag_macro is None:
-        raise HyTypeError(tag, "'{0}' is not a defined tag macro.".format(tag))
+        raise HyTypeError("`{0}' is not a defined tag macro.".format(tag),
+                          None, tag, None)
 
     expr = tag_macro(tree)
 
diff --git a/hy/models.py b/hy/models.py
index cf02dab0b..478c69102 100644
--- a/hy/models.py
+++ b/hy/models.py
@@ -1,16 +1,18 @@
 # Copyright 2019 the authors.
 # This file is part of Hy, which is free software licensed under the Expat
 # license. See the LICENSE.
-
 from __future__ import unicode_literals
+
 from contextlib import contextmanager
 from math import isnan, isinf
+from hy import _initialize_env_var
 from hy._compat import PY3, str_type, bytes_type, long_type, string_types
+from hy.errors import HyWrapperError
 from fractions import Fraction
 from clint.textui import colored
 
-
 PRETTY = True
+_hy_colored_ast_objects = _initialize_env_var('HY_COLORED_AST_OBJECTS', False)
 
 
 @contextmanager
@@ -63,7 +65,7 @@ def wrap_value(x):
 
     new = _wrappers.get(type(x), lambda y: y)(x)
     if not isinstance(new, HyObject):
-        raise TypeError("Don't know how to wrap {!r}: {!r}".format(type(x), x))
+        raise HyWrapperError("Don't know how to wrap {!r}: {!r}".format(type(x), x))
     if isinstance(x, HyObject):
         new = new.replace(x, recursive=False)
     if not hasattr(new, "start_column"):
@@ -271,8 +273,9 @@ def __repr__(self):
         return str(self) if PRETTY else super(HySequence, self).__repr__()
 
     def __str__(self):
+        global _hy_colored_ast_objects
         with pretty():
-            c = self.color
+            c = self.color if _hy_colored_ast_objects else str
             if self:
                 return ("{}{}\n  {}{}").format(
                     c(self.__class__.__name__),
@@ -298,10 +301,12 @@ class HyDict(HySequence):
     """
     HyDict (just a representation of a dict)
     """
+    color = staticmethod(colored.green)
 
     def __str__(self):
+        global _hy_colored_ast_objects
         with pretty():
-            g = colored.green
+            g = self.color if _hy_colored_ast_objects else str
             if self:
                 pairs = []
                 for k, v in zip(self[::2],self[1::2]):
diff --git a/tests/compilers/test_ast.py b/tests/compilers/test_ast.py
index 75e9c499a..9311eef82 100644
--- a/tests/compilers/test_ast.py
+++ b/tests/compilers/test_ast.py
@@ -6,11 +6,10 @@
 from __future__ import unicode_literals
 
 from hy import HyString
-from hy.models import HyObject
 from hy.compiler import hy_compile, hy_eval
-from hy.errors import HyCompileError, HyTypeError
+from hy.errors import HyCompileError, HyLanguageError, HyError
 from hy.lex import hy_parse
-from hy.lex.exceptions import LexException
+from hy.lex.exceptions import LexException, PrematureEndOfInput
 from hy._compat import PY3
 
 import ast
@@ -27,7 +26,7 @@ def _ast_spotcheck(arg, root, secondary):
 
 
 def can_compile(expr):
-    return hy_compile(hy_parse(expr), "__main__")
+    return hy_compile(hy_parse(expr), __name__)
 
 
 def can_eval(expr):
@@ -35,21 +34,16 @@ def can_eval(expr):
 
 
 def cant_compile(expr):
-    try:
-        hy_compile(hy_parse(expr), "__main__")
-        assert False
-    except HyTypeError as e:
-        # Anything that can't be compiled should raise a user friendly
-        # error, otherwise it's a compiler bug.
-        assert isinstance(e.expression, HyObject)
-        assert e.message
-        return e
-    except HyCompileError as e:
+    with pytest.raises(HyError) as excinfo:
+        hy_compile(hy_parse(expr), __name__)
+
+    if issubclass(excinfo.type, HyLanguageError):
+        assert excinfo.value.msg
+        return excinfo.value
+    elif issubclass(excinfo.type, HyCompileError):
         # Anything that can't be compiled should raise a user friendly
         # error, otherwise it's a compiler bug.
-        assert isinstance(e.exception, HyTypeError)
-        assert e.traceback
-        return e
+        return excinfo.value
 
 
 def s(x):
@@ -60,11 +54,9 @@ def test_ast_bad_type():
     "Make sure AST breakage can happen"
     class C:
         pass
-    try:
-        hy_compile(C(), "__main__")
-        assert True is False
-    except TypeError:
-        pass
+
+    with pytest.raises(TypeError):
+        hy_compile(C(), __name__, filename='<string>', source='')
 
 
 def test_empty_expr():
@@ -473,8 +465,8 @@ def test_lambda_list_keywords_kwonly():
         assert code.body[0].args.kw_defaults[1].n == 2
     else:
         exception = cant_compile(kwonly_demo)
-        assert isinstance(exception, HyTypeError)
-        message, = exception.args
+        assert isinstance(exception, HyLanguageError)
+        message = exception.args[0]
         assert message == "&kwonly parameters require Python 3"
 
 
@@ -489,9 +481,9 @@ def test_lambda_list_keywords_mixed():
 
 def test_missing_keyword_argument_value():
     """Ensure the compiler chokes on missing keyword argument values."""
-    with pytest.raises(HyTypeError) as excinfo:
+    with pytest.raises(HyLanguageError) as excinfo:
         can_compile("((fn [x] x) :x)")
-    assert excinfo.value.message == "Keyword argument :x needs a value."
+    assert excinfo.value.msg == "Keyword argument :x needs a value."
 
 
 def test_ast_unicode_strings():
@@ -500,7 +492,7 @@ def test_ast_unicode_strings():
     def _compile_string(s):
         hy_s = HyString(s)
 
-        code = hy_compile([hy_s], "__main__")
+        code = hy_compile([hy_s], __name__, filename='<string>', source=s)
         # We put hy_s in a list so it isn't interpreted as a docstring.
 
         # code == ast.Module(body=[ast.Expr(value=ast.List(elts=[ast.Str(s=xxx)]))])
@@ -541,19 +533,19 @@ def test_ast_bracket_string():
 
 def test_compile_error():
     """Ensure we get compile error in tricky cases"""
-    with pytest.raises(HyTypeError) as excinfo:
+    with pytest.raises(HyLanguageError) as excinfo:
         can_compile("(fn [] (in [1 2 3]))")
 
 
 def test_for_compile_error():
     """Ensure we get compile error in tricky 'for' cases"""
-    with pytest.raises(LexException) as excinfo:
+    with pytest.raises(PrematureEndOfInput) as excinfo:
         can_compile("(fn [] (for)")
-    assert excinfo.value.message == "Premature end of input"
+    assert excinfo.value.msg == "Premature end of input"
 
     with pytest.raises(LexException) as excinfo:
         can_compile("(fn [] (for)))")
-    assert excinfo.value.message == "Ran into a RPAREN where it wasn't expected."
+    assert excinfo.value.msg == "Ran into a RPAREN where it wasn't expected."
 
     cant_compile("(fn [] (for [x] x))")
 
@@ -605,13 +597,13 @@ def test_setv_builtins():
 
 
 def test_top_level_unquote():
-    with pytest.raises(HyTypeError) as excinfo:
+    with pytest.raises(HyLanguageError) as excinfo:
         can_compile("(unquote)")
-    assert excinfo.value.message == "The special form 'unquote' is not allowed here"
+    assert excinfo.value.msg == "The special form 'unquote' is not allowed here"
 
-    with pytest.raises(HyTypeError) as excinfo:
+    with pytest.raises(HyLanguageError) as excinfo:
         can_compile("(unquote-splice)")
-    assert excinfo.value.message == "The special form 'unquote-splice' is not allowed here"
+    assert excinfo.value.msg == "The special form 'unquote-splice' is not allowed here"
 
 
 def test_lots_of_comment_lines():
diff --git a/tests/importer/test_importer.py b/tests/importer/test_importer.py
index 3017d1640..dea1baf56 100644
--- a/tests/importer/test_importer.py
+++ b/tests/importer/test_importer.py
@@ -14,10 +14,10 @@
 import pytest
 
 import hy
-from hy.errors import HyTypeError
 from hy.lex import hy_parse
-from hy.lex.exceptions import LexException
-from hy.compiler import hy_compile
+from hy.errors import HyLanguageError
+from hy.lex.exceptions import PrematureEndOfInput
+from hy.compiler import hy_eval, hy_compile
 from hy.importer import HyLoader, cache_from_source
 
 try:
@@ -57,7 +57,7 @@ def test_runpy():
 
 
 def test_stringer():
-    _ast = hy_compile(hy_parse("(defn square [x] (* x x))"), '__main__')
+    _ast = hy_compile(hy_parse("(defn square [x] (* x x))"), __name__)
 
     assert type(_ast.body[0]) == ast.FunctionDef
 
@@ -79,14 +79,8 @@ def _import_test():
 def test_import_error_reporting():
     "Make sure that (import) reports errors correctly."
 
-    def _import_error_test():
-        try:
-            _ = hy_compile(hy_parse("(import \"sys\")"), '__main__')
-        except HyTypeError:
-            return "Error reported"
-
-    assert _import_error_test() == "Error reported"
-    assert _import_error_test() is not None
+    with pytest.raises(HyLanguageError):
+        hy_compile(hy_parse("(import \"sys\")"), __name__)
 
 
 def test_import_error_cleanup():
@@ -124,7 +118,7 @@ def test_import_autocompiles():
 
 def test_eval():
     def eval_str(s):
-        return hy.eval(hy.read_str(s))
+        return hy_eval(hy.read_str(s), filename='<string>', source=s)
 
     assert eval_str('[1 2 3]') == [1, 2, 3]
     assert eval_str('{"dog" "bark" "cat" "meow"}') == {
@@ -205,8 +199,7 @@ def unlink(filename):
         assert mod.a == 11
         assert mod.b == 20
 
-        # Now cause a `LexException`, and confirm that the good module and its
-        # contents stick around.
+        # Now cause a syntax error
         unlink(source)
 
         with open(source, "w") as f:
@@ -214,7 +207,7 @@ def unlink(filename):
             f.write("(setv a 11")
             f.write("(setv b (// 20 1))")
 
-        with pytest.raises(LexException):
+        with pytest.raises(PrematureEndOfInput):
             reload(mod)
 
         mod = sys.modules.get(TESTFN)
diff --git a/tests/macros/test_macro_processor.py b/tests/macros/test_macro_processor.py
index 309ca49c8..134bd5b8d 100644
--- a/tests/macros/test_macro_processor.py
+++ b/tests/macros/test_macro_processor.py
@@ -50,8 +50,7 @@ def test_preprocessor_exceptions():
     """ Test that macro expansion raises appropriate exceptions"""
     with pytest.raises(HyMacroExpansionError) as excinfo:
         macroexpand(tokenize('(defn)')[0], __name__, HyASTCompiler(__name__))
-    assert "_hy_anon_fn_" not in excinfo.value.message
-    assert "TypeError" not in excinfo.value.message
+    assert "_hy_anon_" not in excinfo.value.msg
 
 
 def test_macroexpand_nan():
diff --git a/tests/native_tests/core.hy b/tests/native_tests/core.hy
index a89eece9e..ab8eaf417 100644
--- a/tests/native_tests/core.hy
+++ b/tests/native_tests/core.hy
@@ -687,5 +687,5 @@ result['y in globals'] = 'y' in globals()")
   (doc doc)
   (setv out_err (.readouterr capsys))
   (assert (.startswith (.strip (first out_err))
-            "Help on function (doc) in module hy.core.macros:"))
+            "Help on function doc in module hy.core.macros:"))
   (assert (empty? (second out_err))))
diff --git a/tests/native_tests/language.hy b/tests/native_tests/language.hy
index c237a5a29..65c629ab6 100644
--- a/tests/native_tests/language.hy
+++ b/tests/native_tests/language.hy
@@ -7,7 +7,7 @@
         [sys :as systest]
         re
         [operator [or_]]
-        [hy.errors [HyTypeError]]
+        [hy.errors [HyLanguageError]]
         pytest)
 (import sys)
 
@@ -68,16 +68,16 @@
   "NATIVE: test that setv doesn't work on names Python can't assign to
   and that we can't mangle"
   (try (eval '(setv None 1))
-       (except [e [TypeError]] (assert (in "Can't assign to" (str e)))))
+       (except [e [SyntaxError]] (assert (in "Can't assign to" (str e)))))
   (try (eval '(defn None [] (print "hello")))
-       (except [e [TypeError]] (assert (in "Can't assign to" (str e)))))
+       (except [e [SyntaxError]] (assert (in "Can't assign to" (str e)))))
   (when PY3
     (try (eval '(setv False 1))
-         (except [e [TypeError]] (assert (in "Can't assign to" (str e)))))
+         (except [e [SyntaxError]] (assert (in "Can't assign to" (str e)))))
     (try (eval '(setv True 0))
-         (except [e [TypeError]] (assert (in "Can't assign to" (str e)))))
+         (except [e [SyntaxError]] (assert (in "Can't assign to" (str e)))))
     (try (eval '(defn True [] (print "hello")))
-         (except [e [TypeError]] (assert (in "Can't assign to" (str e)))))))
+         (except [e [SyntaxError]] (assert (in "Can't assign to" (str e)))))))
 
 
 (defn test-setv-pairs []
@@ -87,7 +87,7 @@
   (assert (= b 2))
   (setv y 0 x 1 y x)
   (assert (= y 1))
-  (with [(pytest.raises HyTypeError)]
+  (with [(pytest.raises HyLanguageError)]
     (eval '(setv a 1 b))))
 
 
@@ -144,29 +144,29 @@
     (do
       (eval '(setv (do 1 2) 1))
       (assert False))
-    (except [e HyTypeError]
-      (assert (= e.message "Can't assign or delete a non-expression"))))
+    (except [e HyLanguageError]
+      (assert (= e.msg "Can't assign or delete a non-expression"))))
 
   (try
     (do
       (eval '(setv 1 1))
       (assert False))
-    (except [e HyTypeError]
-      (assert (= e.message "Can't assign or delete a HyInteger"))))
+    (except [e HyLanguageError]
+      (assert (= e.msg "Can't assign or delete a HyInteger"))))
 
   (try
     (do
       (eval '(setv {1 2} 1))
       (assert False))
-    (except [e HyTypeError]
-      (assert (= e.message "Can't assign or delete a HyDict"))))
+    (except [e HyLanguageError]
+      (assert (= e.msg "Can't assign or delete a HyDict"))))
 
   (try
     (do
       (eval '(del 1 1))
       (assert False))
-    (except [e HyTypeError]
-      (assert (= e.message "Can't assign or delete a HyInteger")))))
+    (except [e HyLanguageError]
+      (assert (= e.msg "Can't assign or delete a HyInteger")))))
 
 
 (defn test-no-str-as-sym []
diff --git a/tests/native_tests/native_macros.hy b/tests/native_tests/native_macros.hy
index 1700b5d01..d5c48c78c 100644
--- a/tests/native_tests/native_macros.hy
+++ b/tests/native_tests/native_macros.hy
@@ -3,7 +3,7 @@
 ;; license. See the LICENSE.
 
 (import pytest
-        [hy.errors [HyTypeError]])
+        [hy.errors [HyTypeError HyMacroExpansionError]])
 
 (defmacro rev [&rest body]
   "Execute the `body` statements in reverse"
@@ -66,13 +66,13 @@
   (try
     (eval '(defmacro f [&kwonly a b]))
     (except [e HyTypeError]
-      (assert (= e.message "macros cannot use &kwonly")))
+      (assert (= e.msg "macros cannot use &kwonly")))
     (else (assert False)))
 
   (try
     (eval '(defmacro f [&kwargs kw]))
     (except [e HyTypeError]
-      (assert (= e.message "macros cannot use &kwargs")))
+      (assert (= e.msg "macros cannot use &kwargs")))
     (else (assert False))))
 
 (defn test-fn-calling-macro []
@@ -162,8 +162,8 @@
     ")
   ;; expand the macro twice, should use a different
   ;; gensym each time
-  (setv _ast1 (hy-compile (hy-parse macro1) "foo"))
-  (setv _ast2 (hy-compile (hy-parse macro1) "foo"))
+  (setv _ast1 (hy-compile (hy-parse macro1) __name__))
+  (setv _ast2 (hy-compile (hy-parse macro1) __name__))
   (setv s1 (to_source _ast1))
   (setv s2 (to_source _ast2))
   ;; and make sure there is something new that starts with _;G|
@@ -189,8 +189,8 @@
     ")
   ;; expand the macro twice, should use a different
   ;; gensym each time
-  (setv _ast1 (hy-compile (hy-parse macro1) "foo"))
-  (setv _ast2 (hy-compile (hy-parse macro1) "foo"))
+  (setv _ast1 (hy-compile (hy-parse macro1) __name__))
+  (setv _ast2 (hy-compile (hy-parse macro1) __name__))
   (setv s1 (to_source _ast1))
   (setv s2 (to_source _ast2))
   (assert (in (mangle "_;a|") s1))
@@ -213,8 +213,8 @@
     ")
   ;; expand the macro twice, should use a different
   ;; gensym each time
-  (setv _ast1 (hy-compile (hy-parse macro1) "foo"))
-  (setv _ast2 (hy-compile (hy-parse macro1) "foo"))
+  (setv _ast1 (hy-compile (hy-parse macro1) __name__))
+  (setv _ast2 (hy-compile (hy-parse macro1) __name__))
   (setv s1 (to_source _ast1))
   (setv s2 (to_source _ast2))
   (assert (in (mangle "_;res|") s1))
@@ -224,7 +224,7 @@
   ;; defmacro/g! didn't like numbers initially because they
   ;; don't have a startswith method and blew up during expansion
   (setv macro2 "(defmacro/g! two-point-zero [] `(+ (float 1) 1.0))")
-  (assert (hy-compile (hy-parse macro2) "foo")))
+  (assert (hy-compile (hy-parse macro2) __name__)))
 
 (defn test-defmacro! []
   ;; defmacro! must do everything defmacro/g! can
@@ -243,8 +243,8 @@
     ")
   ;; expand the macro twice, should use a different
   ;; gensym each time
-  (setv _ast1 (hy-compile (hy-parse macro1) "foo"))
-  (setv _ast2 (hy-compile (hy-parse macro1) "foo"))
+  (setv _ast1 (hy-compile (hy-parse macro1) __name__))
+  (setv _ast2 (hy-compile (hy-parse macro1) __name__))
   (setv s1 (to_source _ast1))
   (setv s2 (to_source _ast2))
   (assert (in (mangle "_;res|") s1))
@@ -254,7 +254,7 @@
   ;; defmacro/g! didn't like numbers initially because they
   ;; don't have a startswith method and blew up during expansion
   (setv macro2 "(defmacro! two-point-zero [] `(+ (float 1) 1.0))")
-  (assert (hy-compile (hy-parse macro2) "foo"))
+  (assert (hy-compile (hy-parse macro2) __name__))
 
   (defmacro! foo! [o!foo] `(do ~g!foo ~g!foo))
   ;; test that o! becomes g!
@@ -483,3 +483,37 @@ in expansions."
 
   (test-macro)
   (assert (= blah 1)))
+
+
+(defn test-macro-errors []
+  (import traceback
+          [hy.importer [hy-parse]])
+
+  (setv test-expr (hy-parse "(defmacro blah [x] `(print ~@z)) (blah y)"))
+
+  (with [excinfo (pytest.raises HyMacroExpansionError)]
+    (eval test-expr))
+
+  (setv output (traceback.format_exception_only
+                 excinfo.type excinfo.value))
+  (setv output (cut (.splitlines (.strip (first output))) 1))
+
+  (setv expected ["  File \"<string>\", line 1"
+                  "    (defmacro blah [x] `(print ~@z)) (blah y)"
+                  "                                     ^------^"
+                  "expanding macro blah"
+                  "  NameError: global name 'z' is not defined"])
+
+  (assert (= (cut expected 0 -1) (cut output 0 -1)))
+  (assert (or (= (get expected -1) (get output -1))
+              ;; Handle PyPy's peculiarities
+              (= (.replace (get expected -1) "global " "") (get output -1))))
+
+
+  ;; This should throw a `HyWrapperError` that gets turned into a
+  ;; `HyMacroExpansionError`.
+  (with [excinfo (pytest.raises HyMacroExpansionError)]
+    (eval '(do (defmacro wrap-error-test []
+                 (fn []))
+               (wrap-error-test))))
+  (assert (in "HyWrapperError" (str excinfo.value))))
diff --git a/tests/native_tests/operators.hy b/tests/native_tests/operators.hy
index 716eb7779..e08edbbc3 100644
--- a/tests/native_tests/operators.hy
+++ b/tests/native_tests/operators.hy
@@ -28,7 +28,7 @@
 (defmacro forbid [expr]
   `(assert (try
     (eval '~expr)
-    (except [TypeError] True)
+    (except [[TypeError SyntaxError]] True)
     (else (raise AssertionError)))))
 
 
diff --git a/tests/test_bin.py b/tests/test_bin.py
index 6336122fd..06b3af9fa 100644
--- a/tests/test_bin.py
+++ b/tests/test_bin.py
@@ -6,11 +6,11 @@
 
 import os
 import re
-import sys
 import shlex
 import subprocess
 
 from hy.importer import cache_from_source
+from hy._compat import PY3
 
 import pytest
 
@@ -123,7 +123,16 @@ def test_bin_hy_stdin_as_arrow():
 
 def test_bin_hy_stdin_error_underline_alignment():
     _, err = run_cmd("hy", "(defmacro mabcdefghi [x] x)\n(mabcdefghi)")
-    assert "\n  (mabcdefghi)\n  ^----------^" in err
+
+    msg_idx = err.rindex("    (mabcdefghi)")
+    assert msg_idx
+    err_parts = err[msg_idx:].splitlines()
+    assert err_parts[1].startswith("    ^----------^")
+    assert err_parts[2].startswith("expanding macro mabcdefghi")
+    assert (err_parts[3].startswith("  TypeError: mabcdefghi") or
+            # PyPy can use a function's `__name__` instead of
+            # `__code__.co_name`.
+            err_parts[3].startswith("  TypeError: (mabcdefghi)"))
 
 
 def test_bin_hy_stdin_except_do():
@@ -149,10 +158,66 @@ def test_bin_hy_stdin_unlocatable_hytypeerror():
     # inside run_cmd.
     _, err = run_cmd("hy", """
         (import hy.errors)
-        (raise (hy.errors.HyTypeError '[] (+ "A" "Z")))""")
+        (raise (hy.errors.HyTypeError (+ "A" "Z") None '[] None))""")
     assert "AZ" in err
 
 
+def test_bin_hy_error_parts_length():
+    """Confirm that exception messages print arrows surrounding the affected
+    expression."""
+    prg_str = """
+    (import hy.errors
+            [hy.importer [hy-parse]])
+
+    (setv test-expr (hy-parse "(+ 1\n\n'a 2 3\n\n 1)"))
+    (setv test-expr.start-line {})
+    (setv test-expr.start-column {})
+    (setv test-expr.end-column {})
+
+    (raise (hy.errors.HyLanguageError
+             "this\nis\na\nmessage"
+             test-expr
+             None
+             None))
+    """
+
+    # Up-arrows right next to each other.
+    _, err = run_cmd("hy", prg_str.format(3, 1, 2))
+
+    msg_idx = err.rindex("HyLanguageError:")
+    assert msg_idx
+    err_parts = err[msg_idx:].splitlines()[1:]
+
+    expected = ['  File "<string>", line 3',
+                '    \'a 2 3',
+                '    ^^',
+                'this',
+                'is',
+                'a',
+                'message']
+
+    for obs, exp in zip(err_parts, expected):
+        assert obs.startswith(exp)
+
+    # Make sure only one up-arrow is printed
+    _, err = run_cmd("hy", prg_str.format(3, 1, 1))
+
+    msg_idx = err.rindex("HyLanguageError:")
+    assert msg_idx
+    err_parts = err[msg_idx:].splitlines()[1:]
+    assert err_parts[2] == '    ^'
+
+    # Make sure lines are printed in between arrows separated by more than one
+    # character.
+    _, err = run_cmd("hy", prg_str.format(3, 1, 6))
+    print(err)
+
+    msg_idx = err.rindex("HyLanguageError:")
+    assert msg_idx
+    err_parts = err[msg_idx:].splitlines()[1:]
+    assert err_parts[2] == '    ^----^'
+
+
 def test_bin_hy_stdin_bad_repr():
     # https://github.com/hylang/hy/issues/1389
     output, err = run_cmd("hy", """
@@ -423,3 +488,87 @@ def test_bin_hy_macro_require():
     assert os.path.exists(cache_from_source(test_file))
     output, _ = run_cmd("hy {}".format(test_file))
     assert "abc" == output.strip()
+
+
+def test_bin_hy_tracebacks():
+    """Make sure the printed tracebacks are correct."""
+
+    # We want the filtered tracebacks.
+    os.environ['HY_DEBUG'] = ''
+
+    def req_err(x):
+        assert x == '{}HyRequireError: No module named {}'.format(
+            'hy.errors.' if PY3 else '',
+            (repr if PY3 else str)('not_a_real_module'))
+
+    # Modeled after
+    #   > python -c 'import not_a_real_module'
+    #   Traceback (most recent call last):
+    #     File "<string>", line 1, in <module>
+    #   ImportError: No module named not_a_real_module
+    _, error = run_cmd('hy', '(require not-a-real-module)')
+    error_lines = error.splitlines()
+    if error_lines[-1] == '':
+        del error_lines[-1]
+    assert len(error_lines) <= 10
+      # Rough check for the internal traceback filtering
+    req_err(error_lines[4 if PY3 else -1])
+
+    _, error = run_cmd('hy -c "(require not-a-real-module)"', expect=1)
+    error_lines = error.splitlines()
+    assert len(error_lines) <= 4
+    req_err(error_lines[-1])
+
+    output, error = run_cmd('hy -i "(require not-a-real-module)"')
+    assert output.startswith('=> ')
+    print(error.splitlines())
+    req_err(error.splitlines()[2 if PY3 else -3])
+
+    # Modeled after
+    #   > python -c 'print("hi'
+    #     File "<string>", line 1
+    #       print("hi
+    #               ^
+    #   SyntaxError: EOL while scanning string literal
+    _, error = run_cmd(r'hy -c "(print \""', expect=1)
+    peoi_re = (
+        r'Traceback \(most recent call last\):\n'
+        r'  File "(?:<string>|string-[0-9a-f]+)", line 1\n'
+        r'    \(print "\n'
+        r'           \^\n' +
+        r'{}PrematureEndOfInput: Partial string literal\n'.format(
+            r'hy\.lex\.exceptions\.' if PY3 else ''))
+    assert re.search(peoi_re, error)
+
+    # Modeled after
+    #   > python -i -c "print('"
+    #     File "<string>", line 1
+    #       print('
+    #             ^
+    #   SyntaxError: EOL while scanning string literal
+    #   >>>
+    output, error = run_cmd(r'hy -i "(print \""')
+    assert output.startswith('=> ')
+    assert re.match(peoi_re, error)
+
+    # Modeled after
+    #   > python -c 'print(a)'
+    #   Traceback (most recent call last):
+    #     File "<string>", line 1, in <module>
+    #   NameError: name 'a' is not defined
+    output, error = run_cmd('hy -c "(print a)"', expect=1)
+    error_lines = error.splitlines()
+    assert error_lines[3] == '  File "<string>", line 1, in <module>'
+    # PyPy will add "global" to this error message, so we work around that.
+    assert error_lines[-1].strip().replace(' global', '') == (
+        "NameError: name 'a' is not defined")
+
+    # Modeled after
+    #   > python -c 'compile()'
+    #   Traceback (most recent call last):
+    #     File "<string>", line 1, in <module>
+    #   TypeError: Required argument 'source' (pos 1) not found
+    output, error = run_cmd('hy -c "(compile)"', expect=1)
+    error_lines = error.splitlines()
+    assert error_lines[-2] == '  File "<string>", line 1, in <module>'
+    assert error_lines[-1].startswith('TypeError')
diff --git a/tests/test_lex.py b/tests/test_lex.py
index 19da88bab..f70971933 100644
--- a/tests/test_lex.py
+++ b/tests/test_lex.py
@@ -1,18 +1,46 @@
 # Copyright 2019 the authors.
 # This file is part of Hy, which is free software licensed under the Expat
 # license. See the LICENSE.
+import sys
+import traceback
+
+import pytest
 
 from math import isnan
 from hy.models import (HyExpression, HyInteger, HyFloat, HyComplex, HySymbol,
                        HyString, HyDict, HyList, HySet, HyKeyword)
 from hy.lex import tokenize
 from hy.lex.exceptions import LexException, PrematureEndOfInput
-import pytest
+from hy.errors import hy_exc_handler
 
 def peoi(): return pytest.raises(PrematureEndOfInput)
 def lexe(): return pytest.raises(LexException)
 
 
+def check_ex(execinfo, expected):
+    output = traceback.format_exception_only(execinfo.type, execinfo.value)
+    assert output[:-1] == expected[:-1]
+    # Python 2.7 doesn't give the full exception name, so we compensate.
+    assert output[-1].endswith(expected[-1])
+
+
+def check_trace_output(capsys, execinfo, expected):
+   sys.__excepthook__(execinfo.type, execinfo.value, execinfo.tb)
+   captured_wo_filtering = capsys.readouterr()[-1].strip('\n')
+
+   hy_exc_handler(execinfo.type, execinfo.value, execinfo.tb)
+   captured_w_filtering = capsys.readouterr()[-1].strip('\n')
+
+   output = captured_w_filtering.split('\n')
+
+   # Make sure the filtered frames aren't the same as the unfiltered ones.
+   assert output[:-1] != captured_wo_filtering.split('\n')[:-1]
+   # Remove the origin frame lines.
+   assert output[3:-1] == expected[:-1]
+   # Python 2.7 doesn't give the full exception name, so we compensate.
+   assert output[-1].endswith(expected[-1])
+
+
 def test_lex_exception():
     """ Ensure tokenize throws a fit on a partial input """
     with peoi(): tokenize("(foo")
@@ -30,8 +58,13 @@ def test_unbalanced_exception():
 def test_lex_single_quote_err():
     "Ensure tokenizing \"' \" throws a LexException that can be stringified"
     # https://github.com/hylang/hy/issues/1252
-    with lexe() as e: tokenize("' ")
-    assert "Could not identify the next token" in str(e.value)
+    with lexe() as execinfo:
+        tokenize("' ")
+    check_ex(execinfo, [
+        '  File "<string>", line 1\n',
+        "    '\n",
+        '    ^\n',
+        'LexException: Could not identify the next token.\n'])
 
 
 def test_lex_expression_symbols():
@@ -74,7 +107,11 @@ def test_lex_strings_exception():
     """ Make sure tokenize throws when codec can't decode some bytes"""
     with lexe() as execinfo:
         tokenize('\"\\x8\"')
-    assert "Can't convert \"\\x8\" to a HyString" in str(execinfo.value)
+    check_ex(execinfo, [
+        '  File "<string>", line 1\n',
+        '    "\\x8"\n',
+        '    ^\n',
+        'LexException: Can\'t convert "\\x8" to a HyString\n'])
 
 
 def test_lex_bracket_strings():
@@ -180,7 +217,16 @@ def test_lex_digit_separators():
 
 
 def test_lex_bad_attrs():
-    with lexe(): tokenize("1.foo")
+    with lexe() as execinfo:
+        tokenize("1.foo")
+    check_ex(execinfo, [
+        '  File "<string>", line 1\n',
+        '    1.foo\n',
+        '    ^\n',
+        'LexException: Cannot access attribute on anything other'
+            ' than a name (in order to get attributes of expressions,'
+            ' use `(. <expression> <attr>)` or `(.<attr> <expression>)`)\n'])
+
     with lexe(): tokenize("0.foo")
     with lexe(): tokenize("1.5.foo")
     with lexe(): tokenize("1e3.foo")
@@ -419,3 +465,27 @@ def test_discard():
     assert tokenize("a '#_b c") == [HySymbol("a"), HyExpression([HySymbol("quote"), HySymbol("c")])]
     assert tokenize("a '#_b #_c d") == [HySymbol("a"), HyExpression([HySymbol("quote"), HySymbol("d")])]
     assert tokenize("a '#_ #_b c d") == [HySymbol("a"), HyExpression([HySymbol("quote"), HySymbol("d")])]
+
+
+def test_lex_exception_filtering(capsys):
+    """Confirm that the exception filtering works for lexer errors."""
+
+    # First, test for PrematureEndOfInput
+    with peoi() as execinfo:
+        tokenize(" \n (foo\n       \n")
+    check_trace_output(capsys, execinfo, [
+        '  File "<string>", line 2',
+        '    (foo',
+        '       ^',
+        'PrematureEndOfInput: Premature end of input'])
+
+    # Now, for a generic LexException
+    with lexe() as execinfo:
+        tokenize("  \n\n  1.foo   ")
+    check_trace_output(capsys, execinfo, [
+        '  File "<string>", line 3',
+        '    1.foo',
+        '    ^',
+        'LexException: Cannot access attribute on anything other'
+            ' than a name (in order to get attributes of expressions,'
+            ' use `(. <expression> <attr>)` or `(.<attr> <expression>)`)'])