Skip to content

Latest commit

 

History

History
297 lines (210 loc) · 7.65 KB

exceptions.md

File metadata and controls

297 lines (210 loc) · 7.65 KB

Exception Handling

Exceptions

Exception handling is offered by many programming languages to handle the runtime errors so that the normal flow of the application can be maintained. Venice's exception handling is built on Java exceptions thus providing seamless interoperability with existing Java code.

  1. Exceptions must never be used for flow control.
  2. Exceptions are used when one can't continue.

A few exception types are imported implicitly to simplify usage:

  • :java.lang.Exception
  • :java.lang.RuntimeException
  • :com.github.jlangch.venice.VncException
  • :com.github.jlangch.venice.ValueException

Create exceptions using the function ex :

(do
   (import :java.io.IOException)
   (import :java.text.ParseException)

   ;; create an unchecked RuntimeException
   (ex :RuntimeException "exception message")
  
   ;; create a checked IOException. Will be wrapped with a :RuntimeException 
   ;; when throwing it!
   (ex :IOException "exception message")
   
   ;; create a checked ParseException. Will be wrapped with a :RuntimeException 
   ;; public ParseException(String s, int errorOffset)
   (ex :ParseException "Expected '['" 1000)
   
   ;; create a Venice exception
   (ex :VncException "exception message")
   
   ;; create an exception with a cause
   (let [cause (ex :RuntimeException "exception message")]
     (ex :VncException "exception message" cause))

Note: Prefer using the ex function over Java Interop to create exceptions! ex works even with a full restricted sandbox where as the Java Interop variant requires a specifically configured sandbox to support the exception classes if sandboxing is activated.

Create exceptions using Java Interop:

(do
   (import :java.text.ParseException)

   ;; public ParseException(String s, int errorOffset)
   (. :ParseException :new "Expected '['" 1000)
   
   ;; create an exception with a cause
   (let [cause (ex :RuntimeException "exception message")]
     (. :VncException :new "exception message" cause)))
   

checked vs unchecked exceptions

All exceptions in Venice are unchecked.

If checked exceptions are thrown in Venice they are immediately wrapped in a runtime exception before being thrown!

If Venice catches a checked exception from a Java Interop call it wraps it in a :RuntimeException before handling it by the catch block selectors!

try - catch - finally

catch clauses within a try can catch any Java exception:

The first catch clause that matches the thrown exception will execute.

(do
   (try
      (throw (ex :RuntimeException "a message"))
      (catch :VncException e "VncException, msg: ~(:message e)")
      (catch :RuntimeException e "RuntimeException, msg: ~(:message e)")
      (finally (println "... finally."))))
      
;; output:
;;
;; ... finally.
;; => "RuntimeException msg: a message"

Note: The finally block is just for side effects, like closing resources. It never returns a value!

Throw, catch, and finally blocks may contain multiple expressions:

(do
   (import :java.io.IOException)
  
   (try
      (println "try...")
      (throw (ex :RuntimeException "a message"))
      (catch :RuntimeException e
         (println "caught RuntimeException")
         "RuntimeException, msg: ~(:message e)")
      (finally 
         (println "... finally."))))
      
;; output:
;;
;; try...
;; caught RuntimeException
;; ... finally.
;; => "RuntimeException msg: a message"

Any Venice data value can be thrown resulting in a :ValueException with the data as the exception's value:

(do
   (try
      (throw [1 2 3])  ; ValueException
      (catch :ValueException e (:value e))))
      
;; output:
;;
;; => [1 2 3]

try-with resources

The try-with statement is a try statement that declares one or more named resources in a binding vector. A resource is an object that must be closed after the program is finished with it. The try-with statement ensures that each resource is closed at the end of the statement. Any object that implements :java.lang.AutoCloseable or :java.io.Closeable, can be used as a resource.

(do
   (import :java.io.FileInputStream)
   
   (let [file (io/temp-file "test-", ".txt")]
      (io/spit file "123456789" :append true)
      (try-with [is (. :FileInputStream :new file)]
         (io/slurp-stream is :binary false))))

Selectors

Selectors define the rules for catching specific exceptions in a catch block.

A selector can be:

  • a class: (e.g., :RuntimeException, :java.text.ParseException), matches any instance of that class

  • a key-values vector: (e.g., [key val & kvs]), matches any instance of :ValueException where the exception's value meets the expression (and (= (get ex-value key) val) ...). To match a specific exception cause type use the selector [:cause-type :java.io.IOException]

  • a predicate: (a function of one argument like map?, set?), matches any instance of :ValueException where the predicate applied to the exception's value returns true

Selector Examples

Class selector:

(do
   (try
      (throw (ex :RuntimeException "a message"))
      (catch :VncException e
         (println "VncException, msg: ~(:message e)"))
      (catch :RuntimeException e
         (println "RuntimeException, msg: ~(:message e)"))))

key-value selector:

(do
   (try
      (throw {:a 100, :b 200})
      (catch [:a 100] e
         (println "ValueException, value: ~(:value e)"))
      (catch [:a 100, :b 200] e
         (println "ValueException, value: ~(:value e)"))))

key-value selector matching exception cause type:

(do
   (try
      ;; note: Venice wraps any checked exception with a :RuntimeException
      (throw (ex :java.io.IOException "test"))
      (catch [:cause-type :java.io.IOException] e
         (println "IOException, message: ~(:message (:cause e))"))
      (catch :RuntimeException  e
         (println "RuntimeException, message: ~(:message e)"))))

Predicate selector:

(do
   (try
      (throw {:a 100, :b 200})
      (catch long? e
         (println "ValueException, value: ~(:value e)"))
      (catch map? e
         (println "ValueException, value: ~(:value e)"))
      (catch #(and (map? %) (= 100 (:a %))) e
         (println "ValueException, value: ~(:value e)"))))

Custom Exceptions

Venice Custom Types are a perfect match for custom exceptions. Throw an instance of a custom type as a :ValueException and define a catch clause with a predicate.

Example:

(do
   (deftype :my-exception1 [message :string, position :long]) 
   (deftype :my-exception2 [message :string]) 
   
   (try
      (throw (my-exception1. "error" 100))      
      (catch my-exception1? e 
         (println (:value e)))         
      (catch my-exception2? e 
         (println (:value e)))))

Stack traces

Venice generates user friendly stack traces

(do
   (defn fn1 [x] (fn2 x))
   (defn fn2 [x] (fn3 x))
   (defn fn3 [x] (/ 1 x))
   (fn1 0))
   
=>
Exception in thread "main" VncException: / by zero

[Callstack]
    at: / (user: line 4, col 19)
    at: user/fn3 (user: line 3, col 19)
    at: user/fn2 (user: line 2, col 19)
    at: user/fn1 (user: line 5, col 5)

Railway Oriented Programming

Railway oriented programming (ROP) is a functional approach to the error handling for sequentially executing functions.

Scott Wlaschin ROP Intro

Scott Wlaschin ROP Video NDC London 2014

ROP in Clojure