diff --git a/CHANGELOG b/CHANGELOG
index 95e5b58ff8f0ac6c9c298b197cfc4f9e75268d1e..a3edf69308b1ade4ddbf68164f26210e9ca422ed 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,26 @@
+2020-07-24  David Byers  <david.byers@liu.se>
+
+	Fix issue #4:
+	* olc.el (olc-shorten-compound): Better handling of empty response
+	from nominatim.
+	(olc-recover-compound): Detect empty response from nominatim.
+	(olc-position-of): New function.
+	(olc-encode): Use cl-check-type to detect invalid encoding
+	lengths.
+	(general): Make errors more consistent.
+	(olc-shorten): Correctly signal shortcode and padded errors.
+	(olc-shorten-compound): Correctly signal shortcode and padded
+	errors.
+	(olc-parse-code): Match padding only at pos so parsing continues
+	if there are zeros later on in the code. The correct codes were
+	considred invalid, but we got the wrong errors.
+
+	* olc.texi (Errors): Document all the errors.
+
+	* test/olctest.el: Improved error-checking assertion. Added
+	support for expected failures.
+
+
 2020-07-23  David Byers  <david.byers@liu.se>
 
 	Fix issue #2:
diff --git a/Makefile b/Makefile
index 8dca7ba2610800359c826c8d17a8e67046a3e168..a12b5b49d89ee3e8383925fb10df5219c191f2e0 100644
--- a/Makefile
+++ b/Makefile
@@ -48,4 +48,5 @@ test:
 		-l ../olc.el \
 		-l olctest.el \
 		-f olctest-batch-test \
+		$(TESTS) \
 		)
diff --git a/olc.el b/olc.el
index fa268706af3a2a866b804ef788c713aec3e9d884..85e7cdba4444c0b41d382a16d976f948eec06cf6 100644
--- a/olc.el
+++ b/olc.el
@@ -3,7 +3,7 @@
 ;; Copyright (C) 2020 David Byers
 ;;
 ;; Author: David Byers <david.byers@liu.se>
-;; Version: 1.0.2
+;; Version: 1.1.0
 ;; Package-Requires: ((emacs "25.1"))
 ;; Keywords: extensions, lisp
 ;; URL: https://gitlab.liu.se/davby02/olc
@@ -56,17 +56,74 @@
 ;;; Custom errors:
 
 
-(define-error 'olc-error "Error in open location code.")
+(define-error 'olc-error "Open location code error")
+
 (define-error 'olc-parse-error
-  "Parse error in open location code" 'olc-error)
+  "Error parsing open location code" 'olc-error)
+
+(define-error 'olc-parse-error-unexpected-end
+  "Unexpected end parsing open location code"
+  'olc-parse-error)
+
+(define-error 'olc-parse-error-invalid-character
+  "Invalid character parsing open location code"
+  'olc-parse-error)
+
+(define-error 'olc-parse-error-missing-plus
+  "Missing plus sign parsing open location code"
+  'olc-parse-error)
+
+(define-error 'olc-parse-error-invalid-padding
+  "Invalid padding parsing open location code"
+  'olc-parse-error)
+
+(define-error 'olc-parse-error-padded-shortcode
+  "Padded  short code parsing open location code"
+  'olc-parse-error)
+
+(define-error 'olc-parse-error-digit-after-padding
+  "Unexpected digit after padding parsing open location code"
+  'olc-parse-error)
+
+(define-error 'olc-parse-error-empty-code
+  "Empty code when parsing open location code"
+  'olc-parse-error)
+
 (define-error 'olc-decode-error
-  "Error decoding open location code" 'olc-error)
-(define-error 'olc-encode-error
-  "Error encoding open location code" 'olc-error)
+  "Error decoding open location code"
+  'olc-error)
+
+(define-error 'olc-decode-error-shortcode
+  "Short codes must be recovered before decoding"
+  'olc-decode-error)
+
 (define-error 'olc-shorten-error
-  "Error shortening open location code" 'olc-error)
+  "Error shortening open location code."
+  'olc-error)
+
+(define-error 'olc-shorten-error-shortcode
+  "Code is already shortened"
+  'olc-shorten-error)
+
+(define-error 'olc-shorten-error-padded
+  "Unable to shorten padded codes"
+  'olc-shorten-error)
+
 (define-error 'olc-recover-error
-  "Error recovering open location code" 'olc-error)
+  "Error recovering open location code."
+  'olc-error)
+
+(define-error 'olc-recover-error-reference-search-failed
+  "Reference location search failed"
+  'olc-recover-error)
+
+(define-error 'olc-recover-error-reference-not-found
+  "Reference location not found"
+  'olc-recover-error)
+
+(define-error 'olc-recover-error-invalid-reference
+  "Invalid reference location"
+  'olc-recover-error)
 
 
 ;;; Base 20 digits:
@@ -137,11 +194,21 @@ SPEC is a list consisting of an error to catch, the error to
 raise, and args for the raised error.
 
 \(fn (CATCH SIGNAL &rest ARGS) BODY...)"
-  (declare (indent 1))
+  (declare (indent 1) (debug (listp &rest form)))
   `(condition-case nil
        ,@body
      (,(elt spec 0) (signal ',(elt spec 1) (list ,@(cddr spec))))))
 
+(defsubst olc-position-of (char code)
+  "Find the leftmost position of CHAR in CODE."
+  (let ((index 0))
+    (catch 'result
+      (mapc (lambda (letter)
+              (when (= char letter)
+                (throw 'result index))
+              (setq index (1+ index)))
+            code))))
+
 (defsubst olc-clip-latitude (lat)
   "Clip LAT to -90,90."
   (max -90 (min 90 lat)))
@@ -182,49 +249,50 @@ raise, and args for the raised error.
         (catch 'break
           (while (< pos (length code))
             (olc-transform-error
-                (args-out-of-range olc-parse-error
-                                   "code too short" code (1+ pos))
+                (args-out-of-range olc-parse-error-unexpected-end
+                                   code (1+ pos))
               (cond ((eq (elt code pos) ?+) (throw 'break nil))
                     ((eq (elt code pos) ?0) (throw 'break nil))
                     ((= (length pairs) 4) (throw 'break nil))
                     ((not (olc-valid-char (elt code pos)))
-                     (signal 'olc-parse-error
-                             (list "invalid character" pos code)))
+                     (signal 'olc-parse-error-invalid-character
+                             (list code pos (string (elt code pos)))))
                     ((not (olc-valid-char (elt code (1+ pos))))
-                     (signal 'olc-parse-error
-                             (list "invalid character" (1+ pos) code)))
+                     (signal 'olc-parse-error-invalid-character
+                             (list code (1+ pos)
+                                   (string (elt code (1+ pos))))))
                     (t (setq pairs (cons (cons (elt code pos)
                                                (elt code (1+ pos)))
                                          pairs)))))
             (setq pos (+ pos 2))))
 
         ;; Measure the padding
-        (when (string-match "0+" code pos)
+        (when (eq pos (string-match "0+" code pos))
           (setq pos (match-end 0)
                 padding (- (match-end 0) (match-beginning 0))))
 
         ;; Parse the separator
         (olc-transform-error
-            (args-out-of-range olc-parse-error
-                               "code too short" code pos)
+            (args-out-of-range olc-parse-error-unexpected-end
+                               code pos)
           (if (eq (elt code pos) ?+)
               (setq pos (1+ pos))
-            (signal 'olc-parse-error
-                    (list "missing separator" pos code))))
+            (signal 'olc-parse-error-missing-plus
+                    (list code pos))))
 
         ;; Check the length of the padding
         (unless (and (= (% padding 2) 0)
                      (<= (+ padding (* 2 (length pairs))) 8))
-          (signal 'olc-parse-error
-                  (list "incorrect padding length" pos code)))
+          (signal 'olc-parse-error-invalid-padding
+                  (list code pos)))
 
         ;; Determine if the code is shortened or not
         (setq short (< (+ (* 2 (length pairs)) padding) 8))
 
         ;; We cant be short and have padding (not sure why)
         (when (and short (> padding 0))
-          (signal 'olc-parse-error
-                  (list "padded codes can't be shortened" pos code)))
+          (signal 'olc-parse-error-padded-shortcode
+                  (list code pos)))
 
         ;; Determine the precision of the code
         (setq precision (- 8 padding))
@@ -232,32 +300,38 @@ raise, and args for the raised error.
         ;; Parse what's after the separator
         (when (< pos (length code))
           (when (> padding 0)
-            (signal 'olc-parse-error
-                    (list "padding followed by data" pos code)))
+            (signal 'olc-parse-error-digit-after-padding
+                    (list code pos (string (elt code pos)))))
 
           ;; Parse one more pair
-          (olc-transform-error
-              (args-out-of-range olc-parse-error
-                                 "code too short" code (1+ pos))
-            (setq pairs (cons (cons (elt code pos)
-                                    (elt code (1+ pos)))
-                              pairs)
-                  pos (+ 2 pos)
-                  precision (+ 2 precision)))
+          (olc-transform-error (args-out-of-range
+                                olc-parse-error-unexpected-end
+                                code (1+ pos))
+            (cond ((not (olc-valid-char (elt code pos)))
+                   (signal 'olc-parse-error-invalid-character
+                           (list code pos (string (elt code pos)))))
+                  ((not (olc-valid-char (elt code (1+ pos))))
+                   (signal 'olc-parse-error-invalid-character
+                           (list code (1+ pos) (string (elt code (1+ pos))))))
+                  (t (setq pairs (cons (cons (elt code pos)
+                                             (elt code (1+ pos)))
+                                       pairs)
+                           pos (+ 2 pos)
+                           precision (+ 2 precision))))))
 
           ;; Parse grid
           (while (< pos (length code))
             (cond ((not (olc-valid-char (elt code pos)))
-                   (signal 'olc-parse-error
-                           (list "invalid character" pos code)))
+                   (signal 'olc-parse-error-invalid-character
+                           (list code pos (string (elt code pos)))))
                   ((>= (length grid) 5) (setq pos (1+ pos)))
                   (t (setq grid (cons (elt code pos) grid)
                            pos (1+ pos)
-                           precision (1+ precision))))))
+                           precision (1+ precision)))))
 
         ;; Check for an empty code
         (unless pairs
-          (signal 'olc-parse-error (list "invalid code" 0 code)))
+          (signal 'olc-parse-error-empty-code (list code 0)))
 
         ;; Return the result
         (olc-parse-create :pairs (nreverse pairs)
@@ -339,19 +413,14 @@ invalid."
 (cl-defun olc-encode (lat lon &key (len 10))
   "Encode LAT and LON as a LEN length open location code.
 
-LEN is automatically clipped to between 2 and 15.
-`olc-encode-error' is raised if it is otherwise invalid (i.e. 3,
-5, 7, or 9). If LEN is not specified, it defaults to 10.
-
-Returns an olc-area structure. Raises olc-encode-error if the
-values cannot (legally) be encoded to the selected length."
+LEN is automatically clipped to between 2 and 15. Invalid values
+raise an error."
   (cl-check-type lat number)
   (cl-check-type lon number)
   (cl-check-type len integer)
 
   (setq len (max 2 (min 15 len)))
-  (when (and (< len 11) (/= (% len 2) 0))
-    (signal 'olc-encode-error "invalid encoding length"))
+  (cl-check-type len (member 2 4 6 8 10 11 12 13 14 15))
 
   (setq lat (olc-normalize-latitude lat len)
         lon (olc-normalize-longitude lon))
@@ -413,7 +482,7 @@ differences, however, are extremely small."
 
     ;; We only deal with long codes
     (when (olc-parse-short parse)
-      (signal 'olc-decode-error code))
+      (signal 'olc-decode-error-shortcode (list code)))
 
     ;; Process the pairs
     (mapc (lambda (pair)
@@ -454,14 +523,15 @@ shortened code, of if LIMIT is not positive and even."
   (cl-check-type lon number)
   (cl-check-type limit (member 2 4 6 8 10 12))
 
+  (when (olc-is-short code)
+    (signal 'olc-shorten-error-shortcode
+            (list code)))
+
   (let* ((parse (olc-parse-code code))
          (area (olc-decode parse)))
-    (when (olc-is-short parse)
-      (signal 'olc-shorten-error
-              (list "can't shorten shortened codes" code)))
     (when (< (olc-parse-precision parse) 8)
-      (signal 'olc-shorten-error
-              (list "can't shorten padded codes" code)))
+      (signal 'olc-shorten-error-padded
+              (list code)))
 
     (setq lat (olc-clip-latitude lat)
           lon (olc-normalize-longitude lon))
@@ -503,64 +573,75 @@ faster.
   (cl-check-type limit (member 2 4 6 8 10 12))
   (cl-check-type zoom (or integer listp))
 
-  (save-match-data
-    (let* ((area (olc-decode code))
-           (zoom-lo (cond ((numberp zoom) zoom)
-                          ((listp zoom) (elt zoom 0))
-                          (t (signal 'args-out-of-range zoom))))
-           (zoom-hi (cond ((numberp zoom) zoom)
-                          ((listp zoom) (elt zoom 1))
-                          (t (signal 'args-out-of-range zoom))))
-           result)
-
-      ;; Check that zoom range is not inverted
-      (when (or (< zoom-hi zoom-lo)
-                (< zoom-hi 1) (> zoom-hi 18)
-                (< zoom-lo 1) (> zoom-lo 18))
-        (signal 'args-out-of-range zoom))
-
-      ;; Otherwise we may never hit the high limit
-      (setq zoom-hi (1+ zoom-hi))
-
-      (catch 'result
-        (while (< zoom-lo zoom-hi)
-          (let* ((zoom (floor (+ zoom-lo zoom-hi) 2))
-                 (resp (request-response-data
-                        (request
-                          "https://nominatim.openstreetmap.org/reverse"
-                          :params `((lat . ,(olc-area-lat area))
-                                    (lon . ,(olc-area-lon area))
-                                    (zoom . ,zoom)
-                                    (format . "json"))
-                          :parser #'json-read
-                          :sync t)))
-                 (tmp-code
-                  (when resp
-                    (olc-shorten code
-                                 (string-to-number
-                                  (alist-get 'lat resp))
-                                 (string-to-number
-                                  (alist-get 'lon resp))
-                                 :limit limit)))
-                 (padlen (when (string-match "+" tmp-code)
-                           (- 8 (match-beginning 0)))))
+  (when (olc-is-short code)
+    (signal 'olc-shorten-error-shortcode
+            (list code)))
+
+  (let* ((parse (olc-parse-code code))
+         (area (olc-decode code))
+         (zoom-lo (cond ((numberp zoom) zoom)
+                        ((listp zoom) (elt zoom 0))
+                        (t (signal 'args-out-of-range (list '(1 18) zoom)))))
+         (zoom-hi (cond ((numberp zoom) zoom)
+                        ((listp zoom) (elt zoom 1))
+                        (t (signal 'args-out-of-range (list '(1 18) zoom)))))
+         result)
+
+    ;; Check for padding
+    (when (< (olc-parse-precision parse) 8)
+      (signal 'olc-shorten-error-padded
+              (list code)))
+
+    ;; Check that zoom range is not inverted
+    (when (or (< zoom-hi zoom-lo)
+              (< zoom-hi 1) (> zoom-hi 18)
+              (< zoom-lo 1) (> zoom-lo 18))
+      (signal 'args-out-of-range (list '(1 18) zoom)))
+
+    ;; Otherwise we may never hit the high limit
+    (setq zoom-hi (1+ zoom-hi))
+
+    (catch 'result
+      (while (< zoom-lo zoom-hi)
+        (let* ((zoom (floor (+ zoom-lo zoom-hi) 2))
+               (resp (request-response-data
+                      (request
+                        "https://nominatim.openstreetmap.org/reverse"
+                        :params `((lat . ,(olc-area-lat area))
+                                  (lon . ,(olc-area-lon area))
+                                  (zoom . ,zoom)
+                                  (format . "json"))
+                        :parser #'json-read
+                        :sync t)))
+               (tmp-code
+                (when resp
+                  (olc-shorten code
+                               (string-to-number (alist-get 'lat resp))
+                               (string-to-number (alist-get 'lon resp))
+                               :limit limit)))
+               (padlen
+                (when tmp-code (- 8 (olc-position-of ?+ tmp-code)))))
+
+          ;; If resp is nil, then there's no point in going further
+
+          (if (null resp)
+              (setq zoom-lo zoom-hi)
 
             ;; Keep the shortest code we see that has at most limit
             ;; chars removed
+
             (when (and (<= padlen limit)
                        (or (null result)
                            (< (length tmp-code) (length (car result)))))
-              (setq result (cons tmp-code
-                                 (alist-get 'display_name resp))))
+              (setq result (cons tmp-code (alist-get 'display_name resp))))
 
             ;; Zoom in or out
             (if (< padlen limit)
                 (setq zoom-lo (1+ zoom))
-              (setq zoom-hi zoom))))
-        (if (and result (> 8 (progn (string-match "+" (car result))
-                                    (match-end 0))))
-            (concat (car result) " " (cdr result))
-          code)))))
+              (setq zoom-hi zoom)))))
+      (if (and result (< (olc-position-of ?+ (car result)) 8))
+          (concat (car result) " " (cdr result))
+        code))))
 
 
 (cl-defun olc-recover (code lat lon &key (format 'area))
@@ -622,7 +703,7 @@ If FORMAT is `area' (or any other value), the returned value is an
 full open location code."
   ;; Make sure we can do requests
   (save-match-data
-    (unless (fboundp 'request) (signal 'void-function  'request))
+    (unless (fboundp 'request) (signal 'void-function '(request)))
 
     ;; Check types (defer check of ref)
     (cl-check-type code stringp)
@@ -648,9 +729,12 @@ full open location code."
 
         ;; Check that we got a response
         (unless (eq 200 (request-response-status-code resp))
-          (signal 'olc-recover-error
-                  (list "error decoding reference"
-                        (request-response-status-code resp))))
+          (signal 'olc-recover-error-reference-search-failed
+                  (list code ref)))
+
+        (unless (> (length (request-response-data resp)) 0)
+          (signal 'olc-recover-error-reference-not-found
+                  (list code ref)))
 
         (let* ((data (elt (request-response-data resp) 0))
                (lat (alist-get 'lat data))
@@ -658,9 +742,8 @@ full open location code."
 
           ;; Check that we have a lat and lon
           (unless (and lat lon)
-            (signal 'olc-recover-error
-                    (list "reference location missing lat or lon"
-                          data)))
+            (signal 'olc-recover-error-invalid-reference
+                    (list code ref)))
 
           ;; Finally recover the code!
           (olc-recover code
diff --git a/olc.info b/olc.info
index 5a13a41ae3d786c9388264a2e5a275540d56f3b0..fcfb028bcb60a005970e3af96a0f0b38827f0922 100644
--- a/olc.info
+++ b/olc.info
@@ -22,11 +22,12 @@ languages, see https://github.com/google/open-location-code.
 * Menu:
 
 * Data types::  Data types defined by olc.
+* Errors::      Errors raised by olc.
 * Functions::   Functions defined by olc.
 * Index::       Type and function index.
 
 
-File: olc.info,  Node: Data types,  Next: Functions,  Prev: Top,  Up: Top
+File: olc.info,  Node: Data types,  Next: Errors,  Prev: Top,  Up: Top
 
 Data types
 **********
@@ -107,7 +108,101 @@ The olc-parse is a structure mainly used internally.  Unless you call
      shortened, but don’t count on this.
 
 
-File: olc.info,  Node: Functions,  Prev: Data types,  Up: Top
+File: olc.info,  Node: Errors,  Next: Functions,  Prev: Data types,  Up: Top
+
+Errors
+======
+
+‘olc-error’
+     The message is ‘Open location code error’.  This is the parent of
+     all errors in olc.
+
+‘olc-parse-error’
+     The message is ‘Error parsing open location code’.  This is the
+     parent of the various parse errors.  All parse errors have the same
+     associated data: (CODE POS DATA...), where POS is the approximate
+     position of the parse error, CODE is the code being parsed, and
+     DATA depends on the error.
+
+‘olc-parse-error-unexpected-end’
+     The message is ‘Unexpected end parsing open location code’.  Raised
+     when the code is incomplete.
+
+‘olc-parse-error-invalid-character’
+     The message is ‘Invalid character parsing open location code’.
+     Raised when an invalid character is encountered.  DATA is a string
+     containing the invalid character.
+
+‘olc-parse-error-missing-plus’
+     The message is ‘Missing plus sign parsing open location code’.
+     Raised when the plus sign is missing.
+
+‘olc-parse-error-invalid-padding’
+     The message is ‘Invalid padding parsing open location code’.
+     Raised when the padding is invalid (e.g.  odd in length).
+
+‘olc-parse-error-padded-shortcode’
+     The message is ‘Padded short code parsing open location code’.
+     Raised when parsing a code with padding that has been shortened.
+
+‘olc-parse-error-digit-after-padding’
+     The message is ‘Unexpected digit after padding parsing open
+     location code’.  Raised when an unexpected digit is encountered
+     (e.g.  after padding).
+
+‘olc-parse-error-empty-code’
+     The message is ‘Empty code when parsing open location code’.
+     Raised when the code is empty (i.e.  ‘+’).
+
+‘olc-decode-error’
+     The message is ‘Error decoding open location code’.  The associated
+     data is a list containing the code being decoded as its first
+     element.  This is the parent of the various decoding errors raised
+     by ‘olc-decode’.
+
+‘olc-decode-error-shortcode’
+     The message is ‘Short codes must be recovered before decoding’.
+     This is raised when an attempt is made to decode a shortened code.
+
+‘olc-shorten-error’
+     The message is ‘Error shortening open location code’.  The
+     associated data is a list containing the code being decoded as its
+     first element.  This is the parent of the various shortening errors
+     raised by ‘olc-shorten’ and ‘olc-shorten-compound’.
+
+‘olc-shorten-error-shortcode’
+     The message is ‘Code is already shortened’.  Raised when attempting
+     to shorten a shortened code.
+
+‘olc-shorten-error-padding’
+     The message is ‘Unable to shorten padded codes’.  Raised when
+     attempting to shorten a code with padding.
+
+‘olc-recover-error’
+     The message is ‘Error recovering open location code’.  The
+     associated data depends on the exact code, but is always a list
+     with the code being recovered as its first element.  This is the
+     parent of the various recovery errors raised by ‘olc-recover’ and
+     ‘olc-recover-compound’.
+
+‘olc-recover-error-geocoding-request’
+     The messags is ‘Reference location search failed’.  Raised when
+     geographical search fails due to an error at the HTTP layer.  The
+     associated data is (CODE, REF) where REF is the reference being
+     searched.
+
+‘olc-recover-error-geocoding-not-found’
+     The messags is ‘Reference location not found’.  Raised when
+     geographical search returns no result.  The associated data is
+     (CODE, REF) where REF is the reference being searched.
+
+‘olc-recover-error-geocoding-invalid’
+     The messags is ‘Invalid reference location’.  Raised when
+     geographical search returns an invalid result.  The associated data
+     is (CODE, REF) where REF is the reference being searched.
+
+
+File: olc.info,  Node: Functions,  Next: Index,  Prev: Errors,  Up: Top
 
 Functions
 =========
@@ -264,11 +359,12 @@ Index
 
 Tag Table:
 Node: Top67
-Node: Data types861
-Node: olc-area1242
-Node: olc-parse2170
-Node: Functions3400
-Node: Index8754
+Node: Data types899
+Node: olc-area1277
+Node: olc-parse2205
+Node: Errors3435
+Node: Functions7375
+Node: Index12739
 
 End Tag Table
 
diff --git a/olc.texi b/olc.texi
index c86716f99fa35c8a39f2e319bf9e8731c9020cb0..cf5fa147c35dbf8da69aec3c6c5368eed02bb125 100644
--- a/olc.texi
+++ b/olc.texi
@@ -26,11 +26,12 @@ languages, see https://github.com/google/open-location-code.
 
 @menu
 * Data types::  Data types defined by olc.
+* Errors::      Errors raised by olc.
 * Functions::   Functions defined by olc.
 * Index::       Type and function index.
 @end menu
 
-@node Data types,Functions,,Top
+@node Data types,Errors,,Top
 @unnumbered Data types
 
 olc defines two data types: olc-area and olc-parse. The former
@@ -117,7 +118,105 @@ but don't count on this.
 @end defun
 
 
-@node Functions,,Data types,Top
+@node Errors,Functions,Data types,Top
+@unnumberedsec Errors
+
+@table @code
+@item olc-error
+The message is @samp{Open location code error}. This is the parent of
+all errors in olc.
+
+@item olc-parse-error
+The message is @samp{Error parsing open location code}. This is the
+parent of the various parse errors. All parse errors have the same
+associated data: (@var{code} @var{pos} @var{data}...), where @var{pos}
+is the approximate position of the parse error, @var{code} is the
+code being parsed, and @var{data} depends on the error.
+
+@item olc-parse-error-unexpected-end
+The message is @samp{Unexpected end parsing open location
+code}. Raised when the code is incomplete.
+
+@item olc-parse-error-invalid-character
+The message is @samp{Invalid character parsing open location
+code}. Raised when an invalid character is encountered. @var{data} is
+a string containing the invalid character.
+
+@item olc-parse-error-missing-plus
+The message is @samp{Missing plus sign parsing open location
+code}. Raised when the plus sign is missing.
+
+@item olc-parse-error-invalid-padding
+The message is @samp{Invalid padding parsing open location
+code}. Raised when the padding is invalid (e.g. odd in length).
+
+@item olc-parse-error-padded-shortcode
+The message is @samp{Padded short code parsing open location
+code}. Raised when parsing a code with padding that has been
+shortened.
+
+@item olc-parse-error-digit-after-padding
+The message is @samp{Unexpected digit after padding parsing open
+location code}. Raised when an unexpected digit is encountered
+(e.g. after padding).
+
+@item olc-parse-error-empty-code
+The message is @samp{Empty code when parsing open location
+code}. Raised when the code is empty (i.e. @samp{+}).
+
+@item olc-decode-error
+The message is @samp{Error decoding open location code}. The
+associated data is a list containing the code being decoded as its
+first element. This is the parent of the various decoding errors
+raised by @code{olc-decode}.
+
+@item olc-decode-error-shortcode
+The message is @samp{Short codes must be recovered before
+decoding}. This is raised when an attempt is made to decode a
+shortened code.
+
+@item olc-shorten-error
+The message is @samp{Error shortening open location code}. The
+associated data is a list containing the code being decoded as its
+first element. This is the parent of the various shortening errors
+raised by @code{olc-shorten} and @code{olc-shorten-compound}.
+
+@item olc-shorten-error-shortcode
+The message is @samp{Code is already shortened}. Raised when
+attempting to shorten a shortened code.
+
+@item olc-shorten-error-padding
+The message is @samp{Unable to shorten padded codes}. Raised when
+attempting to shorten a code with padding.
+
+@item olc-recover-error
+The message is @samp{Error recovering open location code}. The
+associated data depends on the exact code, but is always a list with
+the code being recovered as its first element. This is the parent of
+the various recovery errors raised by @code{olc-recover} and
+@code{olc-recover-compound}.
+
+@item olc-recover-error-geocoding-request
+The messags is @samp{Reference location search failed}. Raised when
+geographical search fails due to an error at the HTTP layer. The
+associated data is (@var{code}, @var{ref}) where @var{ref} is the
+reference being searched.
+
+@item olc-recover-error-geocoding-not-found
+The messags is @samp{Reference location not found}. Raised when
+geographical search returns no result. The associated data is
+(@var{code}, @var{ref}) where @var{ref} is the reference being
+searched.
+
+@item olc-recover-error-geocoding-invalid
+The messags is @samp{Invalid reference location}. Raised when
+geographical search returns an invalid result. The associated data is
+(@var{code}, @var{ref}) where @var{ref} is the reference being
+searched.
+
+@end table
+
+@node Functions,Index,Errors,Top
 @unnumberedsec Functions
 
 @defun olc-encode lat lon &key length
diff --git a/test/olctest.el b/test/olctest.el
index 0e0f4dbceeb6aa22fd481fd895b3145ebf319a26..5c7ef5df2b26185fc6dcc7c7c757f24aedf28545 100644
--- a/test/olctest.el
+++ b/test/olctest.el
@@ -35,7 +35,7 @@
          (--olctest-current-case ,name))
      (message "olctest running %s" ,name)
      ,@body
-     (olctest-report-results --olctest-results)))
+     (olctest-report-results (reverse --olctest-results))))
 
 
 (cl-defun olctest-record-failure (&key exp act msg)
@@ -47,6 +47,14 @@
                 (act . ,act))
               --olctest-results)))
 
+(defmacro olctest-expect-failure (name &rest body)
+  "Expect a failure."
+  (declare (indent 1) (debug (form &rest form)))
+  `(unless (let ((--olctest-results nil))
+             ,@body
+             --olctest-results)
+     (olctest-record-failure :exp 'failure :act 'success :msg ,name)))
+
 (defun olctest-report-results (results)
   "Report results from tests."
   (if (null results)
@@ -74,7 +82,13 @@
   (declare (indent 1))
   `(when (condition-case --olctest-caught-error
              (progn ,@body t)
-           (,exp nil)
+           ,@(mapcar (lambda (spec)
+                       (cond ((symbolp spec) `(,spec nil))
+                             ((listp spec)
+                              `(,(car spec)
+                                (olctest-equal :exp ',spec :act --olctest-caught-error) nil))
+                             (t (error "invalid olctest error specification"))))
+                     exp)
            (error (olctest-record-failure :exp ',exp :act --olctest-caught-error :msg ,msg) nil))
      (olctest-record-failure :exp ',exp :act 'noerror :msg ,msg)))
 
@@ -244,7 +258,11 @@
              (len (alist-get 'len case))
              (shortcode (alist-get 'exp case))
              (actual (olc-shorten fullcode lat lon :limit len)))
-        (olctest-string= :exp shortcode :act actual :msg len)))))
+        (olctest-string= :exp shortcode :act actual :msg len)))
+
+    (olctest-equal :act (olc-shorten-compound "546FWWM2+F6")
+                   :exp "WWM2+F6 Adamstown, Pitcairn")
+    ))
 
 
 (defun olctest-issue-3 ()
@@ -413,16 +431,94 @@
     ))
 
 
+(defun olctest-errors ()
+  (olctest-testcase "local:errors"
+    (olctest-assert-error (:exp ((olc-parse-error-unexpected-end "22" 2)) :msg "P1")
+      (olc-parse-code "22"))
+
+    (olctest-assert-error (:exp ((olc-parse-error-invalid-character "O2+" 0 "O")) :msg "P2")
+      (olc-parse-code "O2+"))
+
+    (olctest-assert-error (:exp ((olc-parse-error-invalid-character "2O+" 1 "O")) :msg "P3")
+      (olc-parse-code "2O+"))
+
+    (olctest-assert-error (:exp ((olc-parse-error-invalid-character "20+" 1 "0")) :msg "P4")
+      (olc-parse-code "20+"))
+
+    (olctest-assert-error (:exp ((olc-parse-error-unexpected-end "FFFFFFFF" 8)) :msg "P5")
+      (olc-parse-code "FFFFFFFF"))
+
+    (olctest-assert-error (:exp ((olc-parse-error-missing-plus "FFFFFFFFF" 8)) :msg "P6")
+      (olc-parse-code "FFFFFFFFF"))
+
+    (olctest-assert-error (:exp ((olc-parse-error-padded-shortcode "FF0000+" 7)) :msg "P7")
+      (olc-parse-code "FF0000+"))
+
+    (olctest-assert-error (:exp ((olc-parse-error-invalid-padding "FF00000+" 8)) :msg "P8")
+      (olc-parse-code "FF00000+"))
+
+    (olctest-assert-error (:exp ((olc-parse-error-digit-after-padding "FF000000+FF" 9 "F")) :msg "P9")
+      (olc-parse-code "FF000000+FF"))
+
+    (olctest-assert-error (:exp ((olc-parse-error-unexpected-end "FFFFFFFF+F" 10)) :msg "P10")
+      (olc-parse-code "FFFFFFFF+F"))
+
+    (olctest-assert-error (:exp ((olc-parse-error-invalid-character "FFFFFFFF+F0" 10 "0")) :msg "P11")
+      (olc-parse-code "FFFFFFFF+F0"))
+
+    (olctest-assert-error (:exp ((olc-parse-error-invalid-character "FFFFFFFF+FF0" 11 "0")) :msg "P12")
+      (olc-parse-code "FFFFFFFF+FF0"))
+
+    (olctest-assert-error (:exp ((olc-parse-error-empty-code "+" 0)) :msg "P13")
+      (olc-parse-code "+"))
+
+    (olctest-assert-error (:exp ((olc-decode-error-shortcode "22+")) :msg "D1")
+      (olc-decode "22+"))
+
+    (olctest-assert-error (:exp ((olc-shorten-error-padded "22222200+")) :msg "S1")
+      (olc-shorten "22222200+" 0 0))
+
+    (olctest-assert-error (:exp ((olc-shorten-error-shortcode "22+")) :msg "S2")
+      (olc-shorten-compound "22+"))
+
+    (olctest-assert-error (:exp ((olc-shorten-error-padded "FFFFFF00+")) :msg "S3")
+      (olc-shorten-compound "FFFFFF00+"))
+
+    (olctest-assert-error (:exp ((olc-recover-error-reference-not-found
+                                  "22+" "Nowhere Special, Pitcairn")) :msg "R1")
+      (olc-recover-compound "22+ Nowhere Special, Pitcairn"))
+
+    (olctest-expect-failure "R2"
+     (let ((olc-nominatim-url "https://invalid.domain/nominatim"))
+       (olctest-assert-error (:exp ((olc-recover-error-reference-search-failed
+                                     "22+" "Sweden")) :msg "R2")
+         (olc-recover-compound "22+ Sweden"))))
+
+    ))
+
+
+(defvar olctest-selected-tests)
+
+(defmacro run-test (arg)
+  `(if (or (null (ignore-errors olctest-selected-tests))
+           (memq ',arg olctest-selected-tests))
+       (funcall (intern (concat "olctest-" (symbol-name ',arg))))
+     t))
+
 (defun olctest-run-all ()
   "Run all tests."
-  (and (olctest-decode)
-       (olctest-encode)
-       (olctest-shortcodes)
-       (olctest-validity)
-       (olctest-localtests)
-       (olctest-issue-3)
-       (olctest-issue-2)
-       (olctest-issue-1)
+  (let ((olctest-selected-tests
+         (mapcar 'intern command-line-args-left)))
+    (and (run-test decode)
+         (run-test encode)
+         (run-test shortcodes)
+         (run-test validity)
+         (run-test localtests)
+         (run-test errors)
+         (run-test issue-3)
+         (run-test issue-2)
+         (run-test issue-1)
+         )
        ))
 
 (defun olctest-batch-test ()
@@ -431,3 +527,4 @@
            (olctest-run-all)
          (error (message (format "error: %s %s" (car err) (cdr err))) nil))
        0 1)))
+