diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,10 @@
+* 0.1.5 (12 March 2022)
+
+  - Syntax errors in function definitions now result in much more
+    helpful error messages.  Before, it would just complain about
+    something being wrong with the '=' sign.
+    ([#346](https://github.com/disco-lang/disco/issues/346))
+
 * 0.1.4 (10 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.4
+version:             0.1.5
 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
@@ -98,6 +98,8 @@
                      test/error-numpatterns/expected
                      test/error-numpatterns/input
                      test/error-numpatterns/numpatterns.disco
+                     test/error-parse-RHS/expected
+                     test/error-parse-RHS/input
                      test/error-pattype/expected
                      test/error-pattype/input
                      test/error-polyrec/expected
@@ -109,6 +111,8 @@
                      test/error-tyargs/error-tyargs.disco
                      test/error-tyargs/expected
                      test/error-tyargs/input
+                     test/error-unbound/expected
+                     test/error-unbound/input
                      test/error-unboundtyvar/expected
                      test/error-unboundtyvar/input
                      test/error-unboundtyvar/unboundtyvar.disco
@@ -309,6 +313,8 @@
                      test/syntax-types/input
                      test/types-192/expected
                      test/types-192/input
+                     test/types-306/expected
+                     test/types-306/input
                      test/types-bind/expected
                      test/types-bind/input
                      test/types-char-string/expected
diff --git a/src/Disco/Parser.hs b/src/Disco/Parser.hs
--- a/src/Disco/Parser.hs
+++ b/src/Disco/Parser.hs
@@ -461,9 +461,26 @@
 parseTopLevel :: Parser TopLevel
 parseTopLevel = L.nonIndented sc $
       TLDoc  <$> parseDocThing
-  <|> TLDecl <$> try parseDecl
+  <|> TLDecl <$> parseDecl         -- See Note [Parsing definitions and top-level expressions]
   <|> TLExpr <$> thenIndented parseTerm
 
+  -- ~~~~ Note [Parsing definitions and top-level expressions]
+  --
+  -- The beginning of a definition might look the same as an
+  -- expression.  e.g. is f(x,y) the start of a definition of f, or an
+  -- expression with a function call?  We used to therefore wrap
+  -- 'parseDecl' in 'try'.  The problem is that if a definition has a
+  -- syntax error on the RHS, it would fail, backtrack, then try
+  -- parsing a top-level expression and fail when it got to the =
+  -- sign, giving an uninformative parse error message.
+  -- See https://github.com/disco-lang/disco/issues/346.
+  --
+  -- The solution is that we now do more careful backtracking within
+  -- parseDecl itself: when parsing a definition, we only backtrack if
+  -- we don't get a complete LHS + '=' sign; once we start parsing the
+  -- RHS of a definition we no longer backtrack, since it can't
+  -- possibly be a valid top-level expression.
+
 -- | Parse a documentation item: either a group of lines beginning
 --   with @|||@ (text documentation), or a group beginning with @!!!@
 --   (checked examples/properties).
@@ -507,10 +524,15 @@
 -- | Parse a definition of the form @x pat1 .. patn = t@.
 parseDefn :: Parser TermDefn
 parseDefn = label "definition" $
-  TermDefn
-  <$> ident
-  <*> indented ((:[]) <$> (bind <$> many parseAtomicPattern <*> (symbol "=" *> parseTerm)))
+  (\(x, ps) body -> TermDefn x [bind ps body])
 
+  -- Only backtrack if we don't get a complete 'LHS ='.  Once we see
+  -- an = sign, commit to parsing a definition, because it can't be a
+  -- valid standalone expression anymore.  If the RHS fails, we don't
+  -- want to backtrack, we just want to display the parse error.
+  <$> try ((,) <$> ident <*> indented (many parseAtomicPattern) <* reservedOp "=")
+  <*> indented parseTerm
+
 -- | Parse the definition of a user-defined algebraic data type.
 parseTyDefn :: Parser TypeDefn
 parseTyDefn = label "type defintion" $ do
@@ -518,7 +540,7 @@
   indented $ do
     name <- parseTyDef
     args <- fromMaybe [] <$> optional (parens $ parseTyVarName `sepBy1` comma)
-    _ <- symbol "="
+    _ <- reservedOp "="
     TypeDefn name args <$> parseType
 
 -- | Parse the entire input as a term (with leading whitespace and
diff --git a/test/error-parse-RHS/expected b/test/error-parse-RHS/expected
new file mode 100644
--- /dev/null
+++ b/test/error-parse-RHS/expected
@@ -0,0 +1,14 @@
+1:11:
+  |
+1 | f(x) = x +
+  |           ^
+unexpected end of input
+expecting expression or operator
+
+1:18:
+  |
+1 | g(x) = {? 5 if x =< 5, 6 otherwise ?}
+  |                  ^^
+unexpected "=<"
+expecting "?}", "if", "let", "when", ',', expression, operator, or type annotation
+
diff --git a/test/error-parse-RHS/input b/test/error-parse-RHS/input
new file mode 100644
--- /dev/null
+++ b/test/error-parse-RHS/input
@@ -0,0 +1,5 @@
+f : N -> N
+f(x) = x +
+
+g : N -> N
+g(x) = {? 5 if x =< 5, 6 otherwise ?}
diff --git a/test/error-unbound/expected b/test/error-unbound/expected
new file mode 100644
--- /dev/null
+++ b/test/error-unbound/expected
@@ -0,0 +1,1 @@
+Error: encountered undefined name REPL.even. Maybe you haven't defined it yet?
diff --git a/test/error-unbound/input b/test/error-unbound/input
new file mode 100644
--- /dev/null
+++ b/test/error-unbound/input
@@ -0,0 +1,2 @@
+even : Z -> Bool
+even(2)
diff --git a/test/types-306/expected b/test/types-306/expected
new file mode 100644
--- /dev/null
+++ b/test/types-306/expected
@@ -0,0 +1,13 @@
+Error: the shape of two types does not match.
+https://disco-lang.readthedocs.io/en/latest/reference/shape-mismatch.html
+Error: the shape of two types does not match.
+https://disco-lang.readthedocs.io/en/latest/reference/shape-mismatch.html
+Error: values of type Bool → Bool cannot be compared.
+https://disco-lang.readthedocs.io/en/latest/reference/not-qual.html
+Error: values of type a5 → a6 cannot be compared.
+https://disco-lang.readthedocs.io/en/latest/reference/not-qual.html
+Error: the shape of two types does not match.
+https://disco-lang.readthedocs.io/en/latest/reference/shape-mismatch.html
+Error: the shape of two types does not match.
+https://disco-lang.readthedocs.io/en/latest/reference/shape-mismatch.html
+{}
diff --git a/test/types-306/input b/test/types-306/input
new file mode 100644
--- /dev/null
+++ b/test/types-306/input
@@ -0,0 +1,7 @@
+set [\x.x : Bool -> Bool, \x. not x]
+{\x.x : Bool -> Bool, \x. not x}
+{\x.x, \x. not x} : Set (Bool -> Bool)
+{\x.x, \x. not x}
+bag [\x.x : Bool -> Bool, \x. not x]
+[\x.x : Bool -> Bool, \x. not x]
+{} : Set (Bool -> Bool)
