sigildocs

Chapter 9: Error Handling

Learn to handle errors gracefully and debug your programs.

Basic Errors

The simplest way to signal an error:

(error "Something went wrong")
; Error: Something went wrong

(error "Division by zero" x y)
; Error: Division by zero (x y)

This displays a message and returns #f. For more sophisticated handling, use the exceptions library.

Using guard

Import the exceptions module for structured error handling:

(import (sigil error))

The guard form catches exceptions:

(guard (err
        (else
         (println "Caught an error: ~a" err)
         #f))
  (risky-operation))

Handling Specific Errors

(define (safe-divide a b)
  (guard (err
          ((string? err)
           (println "Division error: ~a" err)
           #f)
          (else
           (println "Unknown error")
           #f))
    (if (= b 0)
        (raise "Cannot divide by zero")
        (/ a b))))

(safe-divide 10 2)  ; => 5
(safe-divide 10 0)  ; => #f, prints "Division error: Cannot divide by zero"

Raising Exceptions

(import (sigil error))

(define (require-positive n)
  (unless (> n 0)
    (raise (str "Expected positive, got " n)))
  n)

(require-positive 5)   ; => 5
(require-positive -1)  ; raises exception

Custom Error Types

For more complex applications, define custom error types with structs:

(import (sigil struct)
        (sigil string))

;; Define a custom error type
(define-struct invalid-input
  (value)
  (message))

(define (validate-input input)
  (when (string-blank? input)
    (raise (invalid-input value: input
                          message: "Input cannot be blank"))))

Defensive Programming

Checking Arguments

(define (process-list lst)
  (unless (list? lst)
    (error "Expected a list" lst))
  (unless (not (null? lst))
    (error "List cannot be empty"))
  ;; ... process ...
  )

Optional with Default

> (define (lookup key table default)
    (let ((result (assoc key table)))
      (if result
          (cdr result)
          default)))
> (lookup 'name '((age . 25)) "unknown")
"unknown"
> (lookup 'age '((age . 25)) 0)
25

Null-Safe Operations

> (define (safe-car lst)
    (if (pair? lst)
        (car lst)
        #f))
> (define (safe-cdr lst)
    (if (pair? lst)
        (cdr lst)
        '()))
> (safe-car '(1 2 3))
1
> (safe-car '())
#f
> (safe-cdr '(1 2 3))
(2 3)
> (safe-cdr '())
()

Debugging Techniques

Print Debugging

The simplest technique:

(define (problematic-function x)
  (display "DEBUG: x = ")
  (write x)
  (newline)
  ;; ... rest of function
  )

A Debug Macro

(define-syntax debug
  (lambda (form)
    (let ((expr (cadr form)))
      `(let ((result ,expr))
         (display "DEBUG: ")
         (display ',expr)
         (display " = ")
         (write result)
         (newline)
         result))))

(debug (+ 1 2))
;; Prints: DEBUG: (+ 1 2) = 3
;; Returns: 3

Tracing Calls

(define (trace-calls proc name)
  (lambda args
    (display "CALL: ")
    (display name)
    (display " ")
    (write args)
    (newline)
    (let ((result (apply proc args)))
      (display "RETURN: ")
      (display name)
      (display " => ")
      (write result)
      (newline)
      result)))

(define my-add (trace-calls + "add"))
(my-add 1 2 3)
;; CALL: add (1 2 3)
;; RETURN: add => 6

Common Errors and Solutions

"Unbound variable"

Error: unbound variable: foo

The variable hasn't been defined. Check:

  • Spelling
  • Correct module imported
  • Definition before use

"Expected pair"

Error: car: expected pair but got ()

You called car or cdr on an empty list. Add a null? check:

(if (null? lst)
    '()
    (car lst))

"Arity mismatch"

Error: procedure expects 2 arguments, got 3

You passed the wrong number of arguments. Check the procedure signature.

"Type error"

Error: +: expected number but got "hello"

A procedure received the wrong type. Add type checks or validate inputs.

Pattern: Result Types

Return structured results instead of raising:

(import (sigil struct))

(define-struct result
  (ok?)
  (value)
  (error))

(define (ok value)
  (result ok?: #t value: value error: #f))

(define (err message)
  (result ok?: #f value: #f error: message))

(define (safe-divide a b)
  (if (= b 0)
      (err "Division by zero")
      (ok (/ a b))))

(let ((r (safe-divide 10 2)))
  (if (result-ok? r)
      (result-value r)
      (begin
        (println "~a" (result-error r))
        0)))

Best Practices

  1. Fail fast: Validate inputs early
  2. Be specific: Error messages should say what went wrong
  3. Provide context: Include relevant values in error messages
  4. Handle gracefully: Recover when possible, fail cleanly when not
  5. Test error cases: Write tests for error conditions

Practice Exercises

  1. Write a safe-list-ref that returns a default value for out-of-bounds access.
  2. Add error handling to the config file reader from Chapter 7.
  3. Create a validation library with helpful error messages.

What's Next

With the fundamentals covered, let's start building our text adventure game!

Next: Game Design →