diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,9 @@
+* 0.1.3.1 (5 March 2022)
+
+  - Fix [#340](https://github.com/disco-lang/disco/issues/340): disco
+    no longer crashes when encountering an undefined name at runtime,
+    but prints a nice error message instead.
+
 * 0.1.3.0 (3 March 2022)
 
   - New features or syntax
diff --git a/disco.cabal b/disco.cabal
--- a/disco.cabal
+++ b/disco.cabal
@@ -1,6 +1,6 @@
 cabal-version:       2.4
 name:                disco
-version:             0.1.3.0
+version:             0.1.3.1
 synopsis:            Functional programming language for teaching discrete math.
 description:         Disco is a simple functional programming language for use in
                      teaching discrete math.  Its syntax is designed to be close
@@ -466,7 +466,7 @@
                        QuickCheck >= 2.9 && < 2.15,
                        splitmix >= 0.1 && < 0.2,
                        fgl >= 5.5 && < 5.8,
-                       optparse-applicative >= 0.12 && < 0.17,
+                       optparse-applicative >= 0.12 && < 0.18,
                        oeis >= 0.3.10,
                        algebraic-graphs >= 0.5,
                        pretty-show >= 1.10
@@ -489,7 +489,7 @@
                        containers >= 0.5 && < 0.7,
                        unbound-generics >= 0.3 && < 0.5,
                        lens >= 4.14 && < 5.2,
-                       optparse-applicative >= 0.12 && < 0.17,
+                       optparse-applicative >= 0.12 && < 0.18,
                        oeis >= 0.3.10
 
   default-language:    Haskell2010
diff --git a/src/Disco/Error.hs b/src/Disco/Error.hs
--- a/src/Disco/Error.hs
+++ b/src/Disco/Error.hs
@@ -29,7 +29,7 @@
 import           Polysemy.Reader
 
 import           Disco.Messages
-import           Disco.Names                      (ModuleName)
+import           Disco.Names                      (ModuleName, QName)
 import           Disco.Parser                     (DiscoParseError)
 import           Disco.Pretty
 import           Disco.Typecheck.Solve
@@ -65,9 +65,13 @@
 -- | Errors that can be generated at runtime.
 data EvalError where
 
-  -- | An unbound name.  This shouldn't happen.
-  UnboundError  :: Name core  -> EvalError
+  -- | An unbound name was encountered.
+  UnboundError  :: QName core  -> EvalError
 
+  -- | An unbound name that really shouldn't happen, coming from some
+  --   kind of internal name generation scheme.
+  UnboundPanic  :: Name core   -> EvalError
+
   -- | Division by zero.
   DivByZero     ::              EvalError
 
@@ -128,10 +132,11 @@
 
 prettyEvalError :: Members '[Reader PA, LFresh] r => EvalError -> Sem r Doc
 prettyEvalError = \case
-   UnboundError x ->
+   UnboundPanic x ->
      ("Bug! No variable found named" <+> pretty' x <> ".")
      $+$
      "Please report this as a bug at https://github.com/disco-lang/disco/issues/ ."
+   UnboundError x -> "Error: encountered undefined name" <+> pretty' x <> ". Maybe you haven't defined it yet?"
    DivByZero      -> "Error: division by zero."
    Overflow       -> "Error: that number would not even fit in the universe!"
    NonExhaustive  -> "Error: value did not match any of the branches in a case expression."
@@ -147,6 +152,7 @@
 prettyTCError = \case
 
   -- XXX include some potential misspellings along with Unbound
+  --   see https://github.com/disco-lang/disco/issues/180
   Unbound x      -> vcat
     [ "Error: there is nothing named" <+> pretty' x <> "."
     , rtd "unbound"
diff --git a/src/Disco/Interpret/CESK.hs b/src/Disco/Interpret/CESK.hs
--- a/src/Disco/Interpret/CESK.hs
+++ b/src/Disco/Interpret/CESK.hs
@@ -153,16 +153,12 @@
   Just res -> return res
   Nothing  -> step cesk >>= runCESK
 
-(!!!) :: (Show a, Show b) => Ctx a b -> QName a -> b
-ctx !!! x = case Ctx.lookup' x ctx of
-  Nothing -> error $ "variable " ++ show x ++ " not found in environment\n"
-    ++ ppShow (Ctx.keysSet ctx)
-  Just v  -> v
-
 -- | Advance the CESK machine by one step.
 step :: Members '[Fresh, Random, State Mem] r => CESK -> Sem r CESK
 step cesk = case cesk of
-  (In (CVar x) e k) -> return $ Out (e !!! x) k
+  (In (CVar x) e k) -> case Ctx.lookup' x e of
+    Nothing -> return $ Up (UnboundError x) k
+    Just v  -> return $ Out v k
   (In (CNum d r) _ k) -> return $ Out (VNum d r) k
   (In (CConst OMatchErr) _ k) -> return $ Up NonExhaustive k
   (In (CConst OEmptyGraph) _ k) -> return $ Out (VGraph empty) k
diff --git a/src/Disco/Value.hs b/src/Disco/Value.hs
--- a/src/Disco/Value.hs
+++ b/src/Disco/Value.hs
@@ -303,7 +303,7 @@
   let value = Ctx.lookup' (localName name) e
   case value of
     Just v  -> return (s, ty, v)
-    Nothing -> Left (UnboundError name)
+    Nothing -> Left (UnboundPanic name)
 
 -- | The possible outcomes of a property test, parametrized over
 --   the type of values. A @TestReason@ explains why a proposition
