title | description | canonical |
---|---|---|
Exception | Exceptions and exception handling in ReScript | /docs/manual/v11.0.0/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()};
ReScript has some built-in exceptions:
<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);}
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;}}
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;}}
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;}}
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);
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(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!"}
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.
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.
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...