Files
ob-elixir/tasks/04-error-handling.md

8.6 KiB

Task 04: Error Handling

Phase: 1 - Core (MVP) Priority: High Estimated Time: 1 hour Dependencies: Task 02 (Basic Execution)

Objective

Implement proper error detection and reporting for Elixir code execution, so users get meaningful feedback when their code fails.

Prerequisites

  • Task 02 completed
  • Basic execution working

Background

Currently, Elixir errors are returned as raw output. We need to:

  1. Detect when Elixir reports an error
  2. Extract useful error information
  3. Present errors clearly to the user
  4. Optionally signal Emacs error conditions

Steps

Step 1: Define error patterns

Add to ob-elixir.el:

;;; Error Handling

(defconst ob-elixir--error-regexp
  "^\\*\\* (\\([A-Za-z.]+Error\\))\\(.*\\)"
  "Regexp matching Elixir runtime errors.
Group 1 is the error type, group 2 is the message.")

(defconst ob-elixir--compile-error-regexp
  "^\\*\\* (\\(CompileError\\|TokenMissingError\\|SyntaxError\\))\\(.*\\)"
  "Regexp matching Elixir compile-time errors.")

(defconst ob-elixir--warning-regexp
  "^warning: \\(.*\\)"
  "Regexp matching Elixir warnings.")

Step 2: Define custom error types

(define-error 'ob-elixir-error
  "Elixir evaluation error")

(define-error 'ob-elixir-compile-error
  "Elixir compilation error"
  'ob-elixir-error)

(define-error 'ob-elixir-runtime-error
  "Elixir runtime error"
  'ob-elixir-error)

Step 3: Implement error detection

(defun ob-elixir--detect-error (output)
  "Check OUTPUT for Elixir errors.

Returns a plist with :type, :message, and :line if an error is found.
Returns nil if no error detected."
  (cond
   ;; Compile-time error
   ((string-match ob-elixir--compile-error-regexp output)
    (list :type 'compile
          :error-type (match-string 1 output)
          :message (string-trim (match-string 2 output))
          :full-output output))
   
   ;; Runtime error
   ((string-match ob-elixir--error-regexp output)
    (list :type 'runtime
          :error-type (match-string 1 output)
          :message (string-trim (match-string 2 output))
          :full-output output))
   
   ;; No error
   (t nil)))

Step 4: Implement error formatting

(defun ob-elixir--format-error (error-info)
  "Format ERROR-INFO into a user-friendly message."
  (let ((type (plist-get error-info :type))
        (error-type (plist-get error-info :error-type))
        (message (plist-get error-info :message)))
    (format "Elixir %s: (%s) %s"
            (if (eq type 'compile) "Compile Error" "Runtime Error")
            error-type
            message)))

(defcustom ob-elixir-signal-errors t
  "Whether to signal Emacs errors on Elixir execution failure.

When non-nil, Elixir errors will be signaled as Emacs errors.
When nil, errors are returned as the result string."
  :type 'boolean
  :group 'ob-elixir)

Step 5: Update the execute function

Modify ob-elixir--execute:

(defun ob-elixir--execute (body result-type)
  "Execute BODY as Elixir code.

RESULT-TYPE is either `value' or `output'.
For `value', wraps code to capture return value.
For `output', captures stdout directly.

Returns the result as a string.
May signal `ob-elixir-error' if execution fails and
`ob-elixir-signal-errors' is non-nil."
  (let* ((tmp-file (org-babel-temp-file "ob-elixir-" ".exs"))
         (code (if (eq result-type 'value)
                   (ob-elixir--wrap-for-value body)
                 body)))
    (with-temp-file tmp-file
      (insert code))
    (let ((result (org-babel-eval
                   (format "%s %s"
                           ob-elixir-command
                           (org-babel-process-file-name tmp-file))
                   "")))
      (ob-elixir--process-result result))))

(defun ob-elixir--process-result (result)
  "Process RESULT from Elixir execution.

Checks for errors and handles them according to `ob-elixir-signal-errors'.
Returns the cleaned result string."
  (let ((trimmed (string-trim result))
        (error-info (ob-elixir--detect-error result)))
    (if error-info
        (if ob-elixir-signal-errors
            (signal (if (eq (plist-get error-info :type) 'compile)
                        'ob-elixir-compile-error
                      'ob-elixir-runtime-error)
                    (list (ob-elixir--format-error error-info)))
          ;; Return error as result
          (plist-get error-info :full-output))
      ;; No error, return trimmed result
      trimmed)))

Step 6: Handle warnings

(defcustom ob-elixir-show-warnings t
  "Whether to include warnings in output.

When non-nil, Elixir warnings are included in the result.
When nil, warnings are stripped from the output."
  :type 'boolean
  :group 'ob-elixir)

(defun ob-elixir--strip-warnings (output)
  "Remove warning lines from OUTPUT if configured."
  (if ob-elixir-show-warnings
      output
    (let ((lines (split-string output "\n")))
      (mapconcat #'identity
                 (cl-remove-if (lambda (line)
                                 (string-match-p ob-elixir--warning-regexp line))
                               lines)
                 "\n"))))

Step 7: Add tests

Add to test/test-ob-elixir.el:

;;; Error Handling Tests

(ert-deftest ob-elixir-test-detect-runtime-error ()
  "Test runtime error detection."
  (let ((output "** (RuntimeError) something went wrong"))
    (let ((error-info (ob-elixir--detect-error output)))
      (should error-info)
      (should (eq 'runtime (plist-get error-info :type)))
      (should (equal "RuntimeError" (plist-get error-info :error-type))))))

(ert-deftest ob-elixir-test-detect-compile-error ()
  "Test compile error detection."
  (let ((output "** (CompileError) test.exs:1: undefined function foo/0"))
    (let ((error-info (ob-elixir--detect-error output)))
      (should error-info)
      (should (eq 'compile (plist-get error-info :type)))
      (should (equal "CompileError" (plist-get error-info :error-type))))))

(ert-deftest ob-elixir-test-no-error ()
  "Test that valid output is not detected as error."
  (should-not (ob-elixir--detect-error "42"))
  (should-not (ob-elixir--detect-error "[1, 2, 3]"))
  (should-not (ob-elixir--detect-error "\"hello\"")))

(ert-deftest ob-elixir-test-error-execution ()
  "Test that errors are properly handled during execution."
  (skip-unless (executable-find ob-elixir-command))
  (let ((ob-elixir-signal-errors nil))
    (let ((result (ob-elixir--execute "raise \"test error\"" 'value)))
      (should (string-match-p "RuntimeError" result)))))

(ert-deftest ob-elixir-test-error-signaling ()
  "Test that errors are signaled when configured."
  (skip-unless (executable-find ob-elixir-command))
  (let ((ob-elixir-signal-errors t))
    (should-error (ob-elixir--execute "raise \"test error\"" 'value)
                  :type 'ob-elixir-runtime-error)))

(ert-deftest ob-elixir-test-undefined-function ()
  "Test handling of undefined function error."
  (skip-unless (executable-find ob-elixir-command))
  (let ((ob-elixir-signal-errors nil))
    (let ((result (ob-elixir--execute "undefined_function()" 'value)))
      (should (string-match-p "\\(UndefinedFunctionError\\|CompileError\\)" result)))))

Step 8: Test in an org buffer

Add to test.org:

* Error Handling Tests

** Runtime Error

#+BEGIN_SRC elixir
raise "This is a test error"
#+END_SRC

** Compile Error

#+BEGIN_SRC elixir
def incomplete_function(
#+END_SRC

** Undefined Function

#+BEGIN_SRC elixir
this_function_does_not_exist()
#+END_SRC

** Warning (should still execute)

#+BEGIN_SRC elixir
x = 1
y = 2
x  # y is unused, may generate warning
#+END_SRC

Acceptance Criteria

  • Runtime errors are detected (e.g., raise "error")
  • Compile errors are detected (e.g., syntax errors)
  • Errors are formatted with type and message
  • ob-elixir-signal-errors controls error behavior
  • Warnings are handled according to ob-elixir-show-warnings
  • Valid output is not mistakenly detected as errors
  • All tests pass: make test

Error Types to Handle

Error Type Example Detection
RuntimeError raise "msg" ** (RuntimeError)
ArgumentError Bad function arg ** (ArgumentError)
ArithmeticError Division by zero ** (ArithmeticError)
CompileError Syntax error ** (CompileError)
SyntaxError Invalid syntax ** (SyntaxError)
TokenMissingError Missing end ** (TokenMissingError)
UndefinedFunctionError Unknown function ** (UndefinedFunctionError)

Files Modified

  • ob-elixir.el - Add error handling functions
  • test/test-ob-elixir.el - Add error handling tests

References