Skip to content

Latest commit

 

History

History
498 lines (397 loc) · 11.3 KB

exception.mdx

File metadata and controls

498 lines (397 loc) · 11.3 KB
titledescriptioncanonical
Exception
Exceptions and exception handling in ReScript
/docs/manual/v11.0.0/exception

Exception

Exceptions are just a special kind of variant, thrown in exceptional cases (don't abuse them!). Consider using the option or result type for recoverable errors.

You can create your own exceptions like you'd make a variant (exceptions need to be capitalized too).

<CodeTab labels={["ReScript", "JS Output"]}>

exceptionInputClosed(string) // later onraise(InputClosed("The stream has closed!"))
import*asCaml_exceptionsfrom"./stdlib/caml_exceptions.js";varInputClosed=/* @__PURE__ */Caml_exceptions.create("Playground.InputClosed");throw{RE_EXN_ID: InputClosed,_1: "The stream has closed!",Error: newError()};

Built-in Exceptions

ReScript has some built-in exceptions:

Not_found

<CodeTab labels={["ReScript", "JS Output"]}>

letgetItem= (item: int) =>if (item===3) { // return the found item here1 } else { raise(Not_found) } letresult=try { getItem(2) } catch { | Not_found=>0// Default value if getItem throws }
import*asCaml_js_exceptionsfrom"./stdlib/caml_js_exceptions.js";functiongetItem(item){if(item===3){return1;}throw{RE_EXN_ID: "Not_found",Error: newError()};}varresult;try{result=getItem(2);}catch(raw_exn){varexn=Caml_js_exceptions.internalToOCamlException(raw_exn);if(exn.RE_EXN_ID==="Not_found"){result=0;}else{throwexn;}}

Note that the above is just for demonstration purposes; in reality, you'd return an option<int> directly from getItem and avoid the try altogether.

You can directly match on exceptions while getting another return value from a function:

<CodeTab labels={["ReScript", "JS Output"]}>

switchlist{1, 2, 3}->List.getExn(4) { | item=>Console.log(item) | exceptionNot_found=>Console.log("No such item found!") }
import*asCore__Listfrom"./stdlib/core__List.js";import*asCaml_js_exceptionsfrom"./stdlib/caml_js_exceptions.js";varexit=0;varitem;try{item=Core__List.getExn({hd: 1,tl: {hd: 2,tl: {hd: 3,tl: /* [] */0}}},4);exit=1;}catch(raw_exn){varexn=Caml_js_exceptions.internalToOCamlException(raw_exn);if(exn.RE_EXN_ID==="Not_found"){console.log("No such item found!");}else{throwexn;}}if(exit===1){console.log(item);}

Invalid_argument

Used to check if argument is valid. This exception takes a string.

<CodeTab labels={["ReScript", "JS Output"]}>

letdivide= (a, b) =>ifb==0 { raise(Invalid_argument("Denominator is zero")) } else { a/b } // catch errortrydivide(2, 0)->Console.logcatch { | Invalid_argument(msg) =>Console.log(msg) // Denominator is zero }
import*asCaml_int32from"./stdlib/caml_int32.js";import*asCaml_js_exceptionsfrom"./stdlib/caml_js_exceptions.js";functiondivide(a,b){if(b===0){throw{RE_EXN_ID: "Invalid_argument",_1: "Denominator is zero",Error: newError()};}returnCaml_int32.div(a,b);}try{console.log(divide(2,0));}catch(raw_msg){varmsg=Caml_js_exceptions.internalToOCamlException(raw_msg);if(msg.RE_EXN_ID==="Invalid_argument"){console.log(msg._1);}else{throwmsg;}}

Assert_failure

Raise when you use assert(condition) and condition is false. The arguments are the location of the assert in the source code (file name, line number, column number).

<CodeTab labels={["ReScript", "JS Output"]}>

letdecodeUser= (json: JSON.t) =>switchjson { | Object(userDict) =>switch (userDict->Dict.get("name"), userDict->Dict.get("age")) { | (Some(String(name)), Some(Number(age))) => (name, age->Float.toInt) | _=>assert(false) } | _=>assert(false) } trydecodeUser(%raw("{}"))->Console.logcatch { | Assert_failure(loc) =>Console.log(loc) // ("filename", line, col) }
mport*asCaml_js_exceptionsfrom"./stdlib/caml_js_exceptions.js";functiondecodeUser(json){if(!Array.isArray(json)&&(json===null||typeofjson!=="object")&&typeofjson!=="number"&&typeofjson!=="string"&&typeofjson!=="boolean"){throw{RE_EXN_ID: "Assert_failure",_1: ["playground.res",8,9],Error: newError()};}if(typeofjson==="object"&&!Array.isArray(json)){varmatch=json["name"];varmatch$1=json["age"];if(match!==undefined&&!(!Array.isArray(match)&&(match===null||typeofmatch!=="object")&&typeofmatch!=="number"&&typeofmatch!=="string"&&typeofmatch!=="boolean")&&typeofmatch==="string"&&match$1!==undefined&&!(!Array.isArray(match$1)&&(match$1===null||typeofmatch$1!=="object")&&typeofmatch$1!=="number"&&typeofmatch$1!=="string"&&typeofmatch$1!=="boolean")&&typeofmatch$1==="number"){return[match,match$1|0];}throw{RE_EXN_ID: "Assert_failure",_1: ["playground.res",6,11],Error: newError()};}throw{RE_EXN_ID: "Assert_failure",_1: ["playground.res",8,9],Error: newError()};}try{console.log(decodeUser({}));}catch(raw_loc){varloc=Caml_js_exceptions.internalToOCamlException(raw_loc);if(loc.RE_EXN_ID==="Assert_failure"){console.log(loc._1);}else{throwloc;}}

Failure

Exception raised to signal that the given arguments do not make sense. This exception takes a string as an argument.

<CodeTab labels={["ReScript", "JS Output"]}>

letisValidEmail=email=> { lethasAtSign=String.includes(email, "@") lethasDot=String.includes(email, ".") if!(hasAtSign&&hasDot) { raise(Failure("Invalid email address")) } else { true } } letisValid=tryisValidEmail("rescript.org") catch { | Failure(msg) => { Console.error(msg) false } }
import*asCaml_js_exceptionsfrom"./stdlib/caml_js_exceptions.js";functionisValidEmail(email){varhasAtSign=email.includes("@");varhasDot=email.includes(".");if(hasAtSign&&hasDot){returntrue;}throw{RE_EXN_ID: "Failure",_1: "Invalid email address",Error: newError()};}varisValid;try{isValid=isValidEmail("rescript.org");}catch(raw_msg){varmsg=Caml_js_exceptions.internalToOCamlException(raw_msg);if(msg.RE_EXN_ID==="Failure"){console.error(msg._1);isValid=false;}else{throwmsg;}}

Division_by_zero

Exception raised by integer division and remainder operations when their second argument is zero.

<CodeTab labels={["ReScript", "JS Output"]}>

// ReScript raise `Division_by_zero` if the denominator is zeroletresult=trySome(10/0) catch { | Division_by_zero=>None } Console.log(result) // None
import*asCaml_int32from"./stdlib/caml_int32.js";import*asCaml_js_exceptionsfrom"./stdlib/caml_js_exceptions.js";varresult;try{result=Caml_int32.div(10,0);}catch(raw_exn){varexn=Caml_js_exceptions.internalToOCamlException(raw_exn);if(exn.RE_EXN_ID==="Division_by_zero"){result=undefined;}else{throwexn;}}console.log(result);

Catching JS Exceptions

To distinguish between JavaScript exceptions and ReScript exceptions, ReScript namespaces JS exceptions under the Exn.Error(payload) variant. To catch an exception thrown from the JS side:

Throw an exception from JS:

// Example.jsexports.someJsFunctionThatThrows=()=>{thrownewError("A Glitch in the Matrix!");}

Then catch it from ReScript:

// import the method in Example.js @module("./Example") externalsomeJsFunctionThatThrows: () =>unit="someJsFunctionThatThrows"try { // call the external methodsomeJSFunctionThatThrows() } catch { | Exn.Error(obj) =>switchExn.message(obj) { | Some(m) =>Console.log("Caught a JS exception! Message: "++m) | None=> () } }

The obj here is of type Exn.t, intentionally opaque to disallow illegal operations. To operate on obj, do like the code above by using the standard library's Exn module's helpers.

Raise a JS Exception

raise(MyException) raises a ReScript exception. To raise a JavaScript exception (whatever your purpose is), use Exn.raiseError:

<CodeTab labels={["ReScript", "JS Output"]}>

letmyTest= () => { Exn.raiseError("Hello!") }
varJs_exn=require("./stdlib/js_exn.js");functionmyTest(){returnJs_exn.raiseError("Hello!");}

Then you can catch it from the JS side:

// after importing `myTest`...try{myTest()}catch(e){console.log(e.message)// "Hello!"}

Catch ReScript Exceptions from JS

The previous section is less useful than you think; to let your JS code work with your exception-throwing ReScript code, the latter doesn't actually need to throw a JS exception. ReScript exceptions can be used by JS code!

<CodeTab labels={["ReScript", "JS Output"]}>

exceptionBadArgument({myMessage: string}) letmyTest= () => { raise(BadArgument({myMessage: "Oops!"})) }
varCaml_exceptions=require("./stdlib/caml_exceptions.js");varBadArgument=Caml_exceptions.create("Playground.BadArgument");functionmyTest(){throw{RE_EXN_ID: BadArgument,myMessage: "Oops!",Error: newError()};}

Then, in your JS:

// after importing `myTest`...try{myTest()}catch(e){console.log(e.myMessage)// "Oops!"console.log(e.Error.stack)// the stack trace}

Note: RE_EXN_ID is an internal field for bookkeeping purposes. Don't use it on the JS side. Use the other fields.

The above BadArgument exception takes an inline record type. We special-case compile the exception as {RE_EXN_ID, myMessage, Error} for good ergonomics. If the exception instead took ordinary positional arguments, l like the standard library's Invalid_argument("Oops!"), which takes a single argument, the argument is compiled to JS as the field _1 instead. A second positional argument would compile to _2, etc.

Tips & Tricks

When you have ordinary variants, you often don't need exceptions. For example, instead of throwing when item can't be found in a collection, try to return an option<item> (None in this case) instead.

Catch Both ReScript and JS Exceptions in the Same catch Clause

try { someOtherJSFunctionThatThrows() } catch { | Not_found=>...// catch a ReScript exception | Invalid_argument(_) =>...// catch a second ReScript exception | Exn.Error(obj) =>...// catch the JS exception }

This technically works, but hopefully you don't ever have to work with such code...

close