sigildocs

Error Handling Guide

This guide covers Sigil's error handling facilities in depth.

Basic Error Signaling

The simplest way to signal an error:

(error "Something went wrong")

(error "Invalid value" bad-value)

This prints a message and returns #f. For structured error handling, use the exceptions library.

The Exceptions Library

(import (sigil error))

Raising Exceptions

;; Raise any value as an exception
(raise "Something went wrong")
(raise (list 'my-error "details"))

;; Raise with a custom condition
(raise (my-error (message "Bad input")))

Catching with guard

(guard (err
        ;; Pattern clauses
        ((string? err)
         (display "String error: ")
         (display err)
         (newline)
         #f)
        ((my-error? err)
         (handle-my-error err))
        (else
         (display "Unknown error")
         (newline)
         #f))
  ;; Body that might raise
  (risky-operation))

The else clause catches anything not matched by previous clauses.

Re-raising

(guard (err
        ((recoverable? err)
         (recover err))
        (else
         (log-error err)
         (raise err)))  ; Re-raise to outer handler
  (operation))

Custom Conditions

Define structured error types:

(define-record <validation-error>
  (field)
  (message)
  (value))

(define (validate-age age)
  (cond
    ((not (integer? age))
     (raise (validation-error
              (field 'age)
              (message "Age must be an integer")
              (value age))))
    ((< age 0)
     (raise (validation-error
              (field 'age)
              (message "Age cannot be negative")
              (value age))))
    ((> age 150)
     (raise (validation-error
              (field 'age)
              (message "Age seems unrealistic")
              (value age))))
    (else age)))

Handle with pattern matching:

(guard (err
        ((validation-error? err)
         (display "Validation failed for ")
         (display (validation-error-field err))
         (display ": ")
         (display (validation-error-message err))
         (newline)
         #f))
  (validate-age input))

Exception Handler Stack

Use with-exception-handler for low-level control:

(with-exception-handler
  (lambda (condition)
    ;; Handle or re-raise
    (if (can-handle? condition)
        (recover condition)
        (raise condition)))
  (lambda ()
    ;; Protected code
    (risky-operation)))

Handlers are stacked — inner handlers run first:

(with-exception-handler outer-handler
  (lambda ()
    (with-exception-handler inner-handler
      (lambda ()
        (raise 'error)))))  ; inner-handler called first

Error Recovery Strategies

Default Values

(define (safe-divide a b)
  (guard (err (else 0))
    (/ a b)))

(safe-divide 10 2)  ; => 5
(safe-divide 10 0)  ; => 0

Retry with Limits

(define (retry-operation op max-attempts)
  (let loop ((attempts 0))
    (guard (err
            (else
             (if (< attempts max-attempts)
                 (begin
                   (display "Retry ")
                   (display (+ attempts 1))
                   (newline)
                   (loop (+ attempts 1)))
                 (raise err))))  ; Give up
      (op))))

Result Types

Avoid exceptions for expected failures:

(define-record <result>
  (ok?)
  (value)
  (error))

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

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

(define (parse-int str)
  (let ((n (string->number str)))
    (if n
        (ok n)
        (err (str "Not a number: " str)))))

;; Usage
(let ((r (parse-int input)))
  (if (result-ok? r)
      (process (result-value r))
      (display (result-error r))))

Option Types

For "might not exist" scenarios:

(define (find-user id)
  ;; Returns user or #f
  (assq id user-table))

(define (get-user-name id)
  (let ((user (find-user id)))
    (if user
        (user-name user)
        "Unknown")))

Cleanup with Dynamic Wind

Ensure cleanup happens even if errors occur:

(dynamic-wind
  (lambda () (display "Setup\n"))
  (lambda ()
    (display "Body\n")
    (raise 'error))
  (lambda () (display "Cleanup\n")))

;; Prints:
;; Setup
;; Body
;; Cleanup
;; Then raises 'error

Best Practices

1. Fail Fast

Validate inputs at function entry:

(define (process-items items)
  (unless (list? items)
    (raise (type-error (expected 'list) (got items))))
  (unless (not (null? items))
    (raise (value-error (message "Items cannot be empty"))))
  ;; ... process ...
  )

2. Be Specific

Good error messages include:

  • What failed
  • Why it failed
  • What was expected
  • What was received
;; Bad
(error "Invalid input")

;; Good
(error (str "Expected positive integer for 'count', got: " value))

3. Document Errors

In docstrings, list what errors a function might raise:

;;; Read and parse a config file.
;;;
;;; Raises:
;;;   file-not-found - if path doesn't exist
;;;   parse-error - if config syntax is invalid
(define (read-config path)
  ...)

4. Handle at Appropriate Level

Don't catch errors too early:

;; Bad: swallows all errors
(define (process x)
  (guard (err (else #f))
    (step1 x)
    (step2 x)
    (step3 x)))

;; Better: let caller decide
(define (process x)
  (step1 x)
  (step2 x)
  (step3 x))

;; Caller handles appropriately
(guard (err
        ((step1-error? err) ...)
        ((step2-error? err) ...))
  (process x))

5. Log Before Re-raising

(guard (err
        (else
         (log-error "Operation failed" err)
         (raise err)))
  (critical-operation))

Debugging

Stack Traces

When an unhandled error occurs, Sigil prints a stack trace showing:

  • The error message
  • Where it was raised
  • The call stack

Inspecting Errors

(guard (err
        (else
         (display "Error type: ")
         (display (type-of err))
         (newline)
         (write err)
         (newline)
         #f))
  (risky-operation))

Conditional Debugging

(define *debug* #t)

(define (debug-log msg)
  (when *debug*
    (display "[DEBUG] ")
    (display msg)
    (newline)))