about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2023-06-19 20:16:39 +0200
committerbptato <nincsnevem662@gmail.com>2023-06-19 20:17:06 +0200
commit82fb1f70ab275884c42dd769b2af8f9df5389e88 (patch)
treeb7a83ca2e2d22e926959f2525169f2a2e4530e38
parent070cfca46f60c3a00fe6dd66457f454a1a217897 (diff)
downloadchawan-82fb1f70ab275884c42dd769b2af8f9df5389e88.tar.gz
Get rid of the .jserr pragma
-rw-r--r--src/buffer/buffer.nim20
-rw-r--r--src/display/client.nim2
-rw-r--r--src/display/pager.nim2
-rw-r--r--src/html/dom.nim314
-rw-r--r--src/html/htmlparser.nim12
-rw-r--r--src/img/path.nim29
-rw-r--r--src/io/request.nim18
-rw-r--r--src/ips/serialize.nim6
-rw-r--r--src/js/javascript.nim81
-rw-r--r--src/types/url.nim21
-rw-r--r--src/utils/opt.nim5
-rw-r--r--src/xhr/formdata.nim15
12 files changed, 248 insertions, 277 deletions
diff --git a/src/buffer/buffer.nim b/src/buffer/buffer.nim
index e0b24cae..74f0545e 100644
--- a/src/buffer/buffer.nim
+++ b/src/buffer/buffer.nim
@@ -793,17 +793,15 @@ proc cancel*(buffer: Buffer): int {.proxy.} =
 
 #https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#multipart/form-data-encoding-algorithm
 proc serializeMultipartFormData(entries: seq[FormDataEntry]): FormData =
-  {.cast(noSideEffect).}:
-    # This is correct, because newFormData with no params has no side effects.
-    let formData = newFormData()
-    for entry in entries:
-      let name = makeCRLF(entry.name)
-      if entry.isstr:
-        let value = makeCRLF(entry.svalue)
-        formData.append(name, value)
-      else:
-        formData.append(name, entry.value, entry.filename)
-    return formData
+  let formData = newFormData0()
+  for entry in entries:
+    let name = makeCRLF(entry.name)
+    if entry.isstr:
+      let value = makeCRLF(entry.svalue)
+      formData.append(name, value)
+    else:
+      formData.append(name, entry.value, entry.filename)
+  return formData
 
 proc serializePlainTextFormData(kvs: seq[(string, string)]): string =
   for it in kvs:
diff --git a/src/display/client.nim b/src/display/client.nim
index 8196df26..af4b3fb2 100644
--- a/src/display/client.nim
+++ b/src/display/client.nim
@@ -434,7 +434,7 @@ proc newConsole(pager: Pager, tty: File): Console =
       raise newException(Defect, "Failed to open console pipe.")
     let url = newURL("javascript:console.show()")
     result.container = pager.readPipe0(some("text/plain"), none(Charset),
-      pipefd[0], option(url), "Browser console")
+      pipefd[0], option(url.get(nil)), "Browser console")
     var f: File
     if not open(f, pipefd[1], fmWrite):
       raise newException(Defect, "Failed to open file for console pipe.")
diff --git a/src/display/pager.nim b/src/display/pager.nim
index bed2b4b5..857b93dd 100644
--- a/src/display/pager.nim
+++ b/src/display/pager.nim
@@ -674,7 +674,7 @@ proc readPipe0*(pager: Pager, ctype: Option[string], cs: Option[Charset],
     fd: fd,
     contenttype: some(ctype.get("text/plain")),
     charset: cs,
-    location: location.get(newURL("file://-"))
+    location: location.get(newURL("file://-").get)
   )
   let bufferconfig = pager.config.getBufferConfig(source.location)
   return pager.dispatcher.newBuffer(bufferconfig, source, title = title)
diff --git a/src/html/dom.nim b/src/html/dom.nim
index 20b10674..1fa3b043 100644
--- a/src/html/dom.nim
+++ b/src/html/dom.nim
@@ -21,6 +21,7 @@ import io/loader
 import io/promise
 import io/request
 import io/window
+import js/exception
 import js/javascript
 import js/timeout
 import types/blob
@@ -619,25 +620,21 @@ proc quadraticCurveTo(ctx: CanvasRenderingContext2D, cpx, cpy, x,
     y: float64) {.jsfunc.} =
   ctx.state.path.quadraticCurveTo(cpx, cpy, x, y)
 
-proc arcTo(ctx: CanvasRenderingContext2D, x1, y1, x2, y2, radius: float64)
-    {.jsfunc.} =
-  if not ctx.state.path.arcTo(x1, y1, x2, y2, radius):
-    #TODO should be DOMException
-    JS_ERR JS_TypeError, "IndexSizeError"
+proc arcTo(ctx: CanvasRenderingContext2D, x1, y1, x2, y2, radius: float64):
+    Err[DOMException] {.jsfunc.} =
+  return ctx.state.path.arcTo(x1, y1, x2, y2, radius)
 
 proc arc(ctx: CanvasRenderingContext2D, x, y, radius, startAngle,
-    endAngle: float64, counterclockwise = false) {.jsfunc.} =
-  if not ctx.state.path.arc(x, y, radius, startAngle, endAngle,
-      counterclockwise):
-    #TODO should be DOMException
-    JS_ERR JS_TypeError, "IndexSizeError"
+    endAngle: float64, counterclockwise = false): Err[DOMException]
+    {.jsfunc.} =
+  return ctx.state.path.arc(x, y, radius, startAngle, endAngle,
+    counterclockwise)
 
 proc ellipse(ctx: CanvasRenderingContext2D, x, y, radiusX, radiusY, rotation,
-    startAngle, endAngle: float64, counterclockwise = false) {.jsfunc.} =
-  if not ctx.state.path.ellipse(x, y, radiusX, radiusY, rotation, startAngle,
-      endAngle, counterclockwise):
-    #TODO should be DOMException
-    JS_ERR JS_TypeError, "IndexSizeError"
+    startAngle, endAngle: float64, counterclockwise = false): Err[DOMException]
+    {.jsfunc.} =
+  return ctx.state.path.ellipse(x, y, radiusX, radiusY, rotation, startAngle,
+    endAngle, counterclockwise)
 
 proc rect(ctx: CanvasRenderingContext2D, x, y, w, h: float64) {.jsfunc.} =
   ctx.state.path.rect(x, y, w, h)
@@ -981,76 +978,71 @@ proc update(tokenList: DOMTokenList) =
     return
   tokenList.element.attr(tokenList.localName, tokenList.toks.join(' '))
 
-proc add(tokenList: DOMTokenList, tokens: varargs[string]) {.jserr, jsfunc.} =
+func validateDOMToken(tok: string): Err[DOMException] =
+  if tok == "":
+    return err(newDOMException("Got an empty string", "SyntaxError"))
+  if AsciiWhitespace in tok:
+    return err(newDOMException("Got a string containing whitespace",
+      "InvalidCharacterError"))
+
+proc add(tokenList: DOMTokenList, tokens: varargs[string]): Err[DOMException]
+    {.jsfunc.} =
   for tok in tokens:
-    if tok == "":
-      #TODO should be DOMException
-      JS_ERR JS_TypeError, "SyntaxError"
-    if AsciiWhitespace in tok:
-      #TODO should be DOMException
-      JS_ERR JS_TypeError, "InvalidCharacterError"
+    ?validateDOMToken(tok)
   for tok in tokens:
     tokenList.toks.add(tok)
   tokenList.update()
+  return ok()
 
-proc remove(tokenList: DOMTokenList, tokens: varargs[string]) {.jserr, jsfunc.} =
+proc remove(tokenList: DOMTokenList, tokens: varargs[string]):
+    Err[DOMException] {.jsfunc.} =
   for tok in tokens:
-    if tok == "":
-      #TODO should be DOMException
-      JS_ERR JS_TypeError, "SyntaxError"
-    if AsciiWhitespace in tok:
-      #TODO should be DOMException
-      JS_ERR JS_TypeError, "InvalidCharacterError"
+    ?validateDOMToken(tok)
   for tok in tokens:
     let i = tokenList.toks.find(tok)
     if i != -1:
       tokenList.toks.delete(i)
   tokenList.update()
+  return ok()
 
-proc toggle(tokenList: DOMTokenList, token: string, force = none(bool)): bool {.jserr, jsfunc.} =
-  if token == "":
-    #TODO should be DOMException
-    JS_ERR JS_TypeError, "SyntaxError"
-  if AsciiWhitespace in token:
-    #TODO should be DOMException
-    JS_ERR JS_TypeError, "InvalidCharacterError"
+proc toggle(tokenList: DOMTokenList, token: string, force = none(bool)):
+    Result[bool, DOMException] {.jsfunc.} =
+  ?validateDOMToken(token)
   let i = tokenList.toks.find(token)
   if i != -1:
     if not force.get(false):
       tokenList.toks.delete(i)
       tokenList.update()
-      return false
-    return true
+      return ok(false)
+    return ok(true)
   if force.get(true):
     tokenList.toks.add(token)
     tokenList.update()
-    return true
-  return false
+    return ok(true)
+  return ok(false)
 
-proc replace(tokenList: DOMTokenList, token, newToken: string): bool {.jserr, jsfunc.} =
-  if token == "" or newToken == "":
-    #TODO should be DOMException
-    JS_ERR JS_TypeError, "SyntaxError"
-  if AsciiWhitespace in token or AsciiWhitespace in newToken:
-    #TODO should be DOMException
-    JS_ERR JS_TypeError, "InvalidCharacterError"
+proc replace(tokenList: DOMTokenList, token, newToken: string):
+    Result[bool, DOMException] {.jsfunc.} =
+  ?validateDOMToken(token)
+  ?validateDOMToken(newToken)
   let i = tokenList.toks.find(token)
   if i == -1:
-    return false
+    return ok(false)
   tokenList.toks[i] = newToken
   tokenList.update()
-  return true
+  return ok(true)
 
 const SupportedTokensMap = {
   "abcd": @["adsf"] #TODO
 }.toTable()
 
-func supports(tokenList: DOMTokenList, token: string): bool {.jserr, jsfunc.} =
+func supports(tokenList: DOMTokenList, token: string):
+    Result[bool, JSError] {.jsfunc.} =
   if tokenList.localName in SupportedTokensMap:
     let lowercase = token.toLowerAscii()
-    return lowercase in SupportedTokensMap[tokenList.localName]
-  else:
-    JS_ERR JS_TypeError, "No supported tokens defined for attribute " & tokenList.localName
+    return ok(lowercase in SupportedTokensMap[tokenList.localName])
+  return err(newTypeError("No supported tokens defined for attribute " &
+    tokenList.localName))
 
 func `$`(tokenList: DOMTokenList): string {.jsfunc.} =
   return tokenList.toks.join(' ')
@@ -1796,7 +1788,7 @@ func newHTMLElement*(document: Document, localName: string,
 func newDocument*(): Document {.jsctor.} =
   result = Document(
     nodeType: DOCUMENT_NODE,
-    url: newURL("about:blank"),
+    url: newURL("about:blank").get,
     index: -1
   )
   result.document = result
@@ -1842,7 +1834,7 @@ func baseURL*(document: Document): Url =
   if href == "":
     return document.url
   if document.url == nil:
-    return newURL("about:blank") #TODO ???
+    return newURL("about:blank").get #TODO ???
   let url = parseURL(href, some(document.url))
   if url.isNone:
     return document.url
@@ -1960,20 +1952,30 @@ proc attrulgz(element: Element, name: string, value: uint32) =
   if value > 0:
     element.attrul(name, value)
 
-proc setAttribute(element: Element, qualifiedName, value: string) {.jserr, jsfunc.} =
-  if not qualifiedName.matchNameProduction():
-    #TODO should be DOMException
-    JS_ERR JS_TypeError, "InvalidCharacterError"
+func validateAttributeName(name: string, isq: static bool = false):
+    Err[DOMException] =
+  when isq:
+    if name.matchNameProduction():
+      return ok()
+  else:
+    if name.matchQNameProduction():
+      return ok()
+  return err(newDOMException("Invalid character in attribute name",
+    "InvalidCharacterError"))
+
+proc setAttribute(element: Element, qualifiedName, value: string):
+    Err[DOMException] {.jsfunc.} =
+  ?validateAttributeName(qualifiedName)
   let qualifiedName = if element.namespace == Namespace.HTML and not element.document.isxml:
     qualifiedName.toLowerAscii2()
   else:
     qualifiedName
   element.attr(qualifiedName, value)
+  return ok()
 
-proc setAttributeNS(element: Element, namespace, qualifiedName, value: string) {.jserr, jsfunc.} =
-  if not qualifiedName.matchQNameProduction():
-    #TODO this should be a DOMException
-    JS_ERR JS_TypeError, "InvalidCharacterError"
+proc setAttributeNS(element: Element, namespace, qualifiedName,
+    value: string): Err[DOMException] {.jsfunc.} =
+  ?validateAttributeName(qualifiedName, isq = true)
   let ps = qualifiedName.until(':')
   let prefix = if ps.len < qualifiedName.len: ps else: ""
   let localName = qualifiedName.substr(prefix.len)
@@ -1981,14 +1983,14 @@ proc setAttributeNS(element: Element, namespace, qualifiedName, value: string) {
       prefix == "xml" and namespace != $Namespace.XML or
       (qualifiedName == "xmlns" or prefix == "xmlns") and namespace != $Namespace.XMLNS or
       namespace == $Namespace.XMLNS and qualifiedName != "xmlns" and prefix != "xmlns":
-    #TODO this should be a DOMException
-    JS_ERR JS_TypeError, "NamespaceError"
+    return err(newDOMException("Unexpected namespace", "NamespaceError"))
   element.attr0(qualifiedName, value)
   let i = element.attributes.findAttrNS(namespace, localName)
   if i != -1:
     element.attributes.attrlist[i].value = value
   else:
     element.attributes.attrlist.add(element.newAttr(localName, value, prefix, namespace))
+  return ok()
 
 proc removeAttribute(element: Element, qualifiedName: string) {.jsfunc.} =
   let qualifiedName = if element.namespace == Namespace.HTML and not element.document.isxml:
@@ -2002,10 +2004,9 @@ proc removeAttributeNS(element: Element, namespace, localName: string) {.jsfunc.
   if i != -1:
     element.delAttr(i)
 
-proc toggleAttribute(element: Element, qualifiedName: string, force = none(bool)): bool {.jserr, jsfunc.} =
-  if not qualifiedName.matchNameProduction():
-    #TODO should be DOMException
-    JS_ERR JS_TypeError, "InvalidCharacterError"
+proc toggleAttribute(element: Element, qualifiedName: string,
+    force = none(bool)): Result[bool, DOMException] {.jsfunc.} =
+  ?validateAttributeName(qualifiedName)
   let qualifiedName = if element.namespace == Namespace.HTML and not element.document.isxml:
     qualifiedName.toLowerAscii2()
   else:
@@ -2013,51 +2014,55 @@ proc toggleAttribute(element: Element, qualifiedName: string, force = none(bool)
   if not element.attrb(qualifiedName):
     if force.get(true):
       element.attr(qualifiedName, "")
-      return true
-    return false
+      return ok(true)
+    return ok(false)
   if not force.get(false):
     element.delAttr(qualifiedName)
-    return false
-  return true
+    return ok(false)
+  return ok(true)
 
 proc value(attr: Attr, s: string) {.jsfset.} =
   attr.value = s
   if attr.ownerElement != nil:
     attr.ownerElement.attr0(attr.name, s)
 
-proc setNamedItem(map: NamedNodeMap, attr: Attr): Option[Attr] {.jserr, jsfunc.} =
+proc setNamedItem(map: NamedNodeMap, attr: Attr): Result[Attr, DOMException]
+    {.jsfunc.} =
   if attr.ownerElement != nil and attr.ownerElement != map.element:
-    #TODO should be DOMException
-    JS_ERR JS_TypeError, "InUseAttributeError"
+    return err(newDOMException("Attribute is currently in use",
+      "InUseAttributeError"))
   if attr.name in map.element.attrs:
-    return some(attr)
+    return ok(attr)
   let i = map.findAttr(attr.name)
   if i != -1:
-    result = some(map.attrlist[i])
+    result = ok(map.attrlist[i])
     map.attrlist.delete(i)
+  else:
+    result = ok(nil)
   map.element.attrs[attr.name] = attr.value
   map.attrlist.add(attr)
 
-proc setNamedItemNS(map: NamedNodeMap, attr: Attr): Option[Attr] {.jsfunc.} =
-  map.setNamedItem(attr)
+proc setNamedItemNS(map: NamedNodeMap, attr: Attr): Result[Attr, DOMException]
+    {.jsfunc.} =
+  return map.setNamedItem(attr)
 
-proc removeNamedItem(map: NamedNodeMap, qualifiedName: string): Attr {.jserr, jsfunc.} =
+proc removeNamedItem(map: NamedNodeMap, qualifiedName: string):
+    Result[Attr, DOMException] {.jsfunc.} =
   let i = map.findAttr(qualifiedName)
   if i != -1:
     let attr = map.attrlist[i]
     map.element.delAttr(i)
-    return attr
-  #TODO should be DOMException
-  JS_ERR JS_TypeError, "Not found"
+    return ok(attr)
+  return err(newDOMException("Item not found", "NotFoundError"))
 
-proc removeNamedItemNS(map: NamedNodeMap, namespace, localName: string): Attr {.jserr, jsfunc.} =
+proc removeNamedItemNS(map: NamedNodeMap, namespace, localName: string):
+    Result[Attr, DOMException] {.jsfunc.} =
   let i = map.findAttrNS(namespace, localName)
   if i != -1:
     let attr = map.attrlist[i]
     map.element.delAttr(i)
-    return attr
-  #TODO should be DOMException
-  JS_ERR JS_TypeError, "Not found"
+    return ok(attr)
+  return err(newDOMException("Item not found", "NotFoundError"))
 
 proc id(element: Element, id: string) {.jsfset.} =
   element.id = id
@@ -2202,43 +2207,53 @@ proc insertionSteps(insertedNode: Node) =
       element.resetFormOwner()
 
 # WARNING the ordering of the arguments in the standard is whack so this doesn't match that
-func preInsertionValidity*(parent, node, before: Node): bool =
+func preInsertionValidity*(parent, node, before: Node): Err[DOMException] =
   if parent.nodeType notin {DOCUMENT_NODE, DOCUMENT_FRAGMENT_NODE, ELEMENT_NODE}:
-    # HierarchyRequestError
-    return false
+    return err(newDOMException("Parent must be a document, document fragment, " &
+      "or element", "HierarchyRequestError"))
   if node.isHostIncludingInclusiveAncestor(parent):
-    # HierarchyRequestError
-    return false
+    return err(newDOMException("Parent must be an ancestor", "HierarchyRequestError"))
   if before != nil and before.parentNode != parent:
-    # NotFoundError
-    return false
-  if node.nodeType notin {DOCUMENT_FRAGMENT_NODE, DOCUMENT_TYPE_NODE, ELEMENT_NODE} + CharacterDataNodes:
-    # HierarchyRequestError
-    return false
-  if (node.nodeType == TEXT_NODE and parent.nodeType == DOCUMENT_NODE) or
-      (node.nodeType == DOCUMENT_TYPE_NODE and parent.nodeType != DOCUMENT_NODE):
-    # HierarchyRequestError
-    return false
+    return err(newDOMException("Reference node is not a child of parent",
+      "NotFoundError"))
+  if node.nodeType notin {DOCUMENT_FRAGMENT_NODE, DOCUMENT_TYPE_NODE,
+      ELEMENT_NODE} + CharacterDataNodes:
+    return err(newDOMException("Cannot insert node type",
+      "HierarchyRequestError"))
+  if node.nodeType == TEXT_NODE and parent.nodeType == DOCUMENT_NODE:
+    return err(newDOMException("Cannot insert text into document",
+      "HierarchyRequestError"))
+  if node.nodeType == DOCUMENT_TYPE_NODE and parent.nodeType != DOCUMENT_NODE:
+    return err(newDOMException("Document type can only be inserted into " &
+      "document", "HierarchyRequestError"))
   if parent.nodeType == DOCUMENT_NODE:
     case node.nodeType
     of DOCUMENT_FRAGMENT_NODE:
       let elems = node.countChildren(ELEMENT_NODE)
       if elems > 1 or node.hasChild(TEXT_NODE):
-        # HierarchyRequestError
-        return false
-      elif elems == 1 and (parent.hasChild(ELEMENT_NODE) or before != nil and (before.nodeType == DOCUMENT_TYPE_NODE or before.hasNextSibling(DOCUMENT_TYPE_NODE))):
-        # HierarchyRequestError
-        return false
+        return err(newDOMException("Document fragment has invalid children",
+          "HierarchyRequestError"))
+      elif elems == 1 and (parent.hasChild(ELEMENT_NODE) or
+          before != nil and (before.nodeType == DOCUMENT_TYPE_NODE or
+          before.hasNextSibling(DOCUMENT_TYPE_NODE))):
+        return err(newDOMException("Document fragment has invalid children",
+          "HierarchyRequestError"))
     of ELEMENT_NODE:
-      if parent.hasChild(ELEMENT_NODE) or before != nil and (before.nodeType == DOCUMENT_TYPE_NODE or before.hasNextSibling(DOCUMENT_TYPE_NODE)):
-        # HierarchyRequestError
-        return false
+      if parent.hasChild(ELEMENT_NODE):
+        return err(newDOMException("Document already has an element child",
+          "HierarchyRequestError"))
+      elif before != nil and (before.nodeType == DOCUMENT_TYPE_NODE or
+            before.hasNextSibling(DOCUMENT_TYPE_NODE)):
+        return err(newDOMException("Cannot insert element before document " &
+          "type", "HierarchyRequestError"))
     of DOCUMENT_TYPE_NODE:
-      if parent.hasChild(DOCUMENT_TYPE_NODE) or before != nil and before.hasPreviousSibling(ELEMENT_NODE) or before == nil and parent.hasChild(ELEMENT_NODE):
-        # HierarchyRequestError
-        return false
+      if parent.hasChild(DOCUMENT_TYPE_NODE) or
+          before != nil and before.hasPreviousSibling(ELEMENT_NODE) or
+          before == nil and parent.hasChild(ELEMENT_NODE):
+        return err(newDOMException("Cannot insert document type before " &
+          "an element node", "HierarchyRequestError"))
     else: discard
-  return true # no exception reached
+  return ok() # no exception reached
 
 proc insertNode(parent, node, before: Node) =
   parent.document.adopt(node)
@@ -2282,18 +2297,17 @@ proc insert*(parent, node, before: Node) =
   for node in nodes:
     insertNode(parent, node, before)
 
-proc insertBefore(parent, node, before: Node): Node {.jserr, jsfunc.} =
-  if parent.preInsertionValidity(node, before):
-    let referenceChild = if before == node:
-      node.nextSibling
-    else:
-      before
-    parent.insert(node, referenceChild)
-    return node
-  #TODO use preInsertionValidity result
-  JS_ERR JS_TypeError, "Pre-insertion validity violated"
+proc insertBefore(parent, node, before: Node): Result[Node, DOMException]
+    {.jsfunc.} =
+  ?parent.preInsertionValidity(node, before)
+  let referenceChild = if before == node:
+    node.nextSibling
+  else:
+    before
+  parent.insert(node, referenceChild)
+  return ok(node)
 
-proc appendChild(parent, node: Node): Node {.jsfunc.} =
+proc appendChild(parent, node: Node): Result[Node, DOMException] {.jsfunc.} =
   return parent.insertBefore(node, nil)
 
 proc append*(parent, node: Node) =
@@ -2301,11 +2315,12 @@ proc append*(parent, node: Node) =
 
 #TODO replaceChild
 
-proc removeChild(parent, node: Node): Node {.jsfunc.} =
-  #TODO should be DOMException
+proc removeChild(parent, node: Node): Result[Node, DOMException] {.jsfunc.} =
   if node.parentNode != parent:
-    JS_ERR JS_TypeError, "NotFoundError"
+    return err(newDOMException("Node is not a child of parent",
+      "NotFoundError"))
   node.remove()
+  return ok(node)
 
 proc replaceAll(parent, node: Node) =
   for i in countdown(parent.childList.high, 0):
@@ -2576,10 +2591,11 @@ proc prepare*(element: HTMLScriptElement) =
     element.execute()
 
 #TODO options/custom elements
-proc createElement(document: Document, localName: string): Element {.jserr, jsfunc.} =
+proc createElement(document: Document, localName: string):
+    Result[Element, DOMException] {.jsfunc.} =
   if not localName.matchNameProduction():
-    #TODO DOMException
-    JS_ERR JS_TypeError, "InvalidCharacterError"
+    return err(newDOMException("Invalid character in element name",
+      "InvalidCharacterError"))
   let localName = if not document.isxml:
     localName.toLowerAscii2()
   else:
@@ -2588,20 +2604,23 @@ proc createElement(document: Document, localName: string): Element {.jserr, jsfu
     Namespace.HTML
   else:
     NO_NAMESPACE
-  return document.newHTMLElement(localName, namespace)
+  return ok(document.newHTMLElement(localName, namespace))
 
 #TODO createElementNS
 
 proc createDocumentFragment(document: Document): DocumentFragment {.jsfunc.} =
   return newDocumentFragment(document)
 
-proc createDocumentType(implementation: DOMImplementation, qualifiedName, publicId, systemId: string): DocumentType {.jserr, jsfunc.} =
+proc createDocumentType(implementation: DOMImplementation, qualifiedName,
+    publicId, systemId: string): Result[DocumentType, DOMException] {.jsfunc.} =
   if not qualifiedName.matchQNameProduction():
-    #TODO should be DOMException
-    JS_ERR JS_TypeError, "InvalidCharacterError"
-  return implementation.document.newDocumentType(qualifiedName, publicId, systemId)
+    return err(newDOMException("Invalid character in document type name",
+      "InvalidCharacterError"))
+  return ok(implementation.document.newDocumentType(qualifiedName, publicId,
+    systemId))
 
-proc createHTMLDocument(implementation: DOMImplementation, title = none(string)): Document {.jsfunc.} =
+proc createHTMLDocument(implementation: DOMImplementation, title =
+    none(string)): Document {.jsfunc.} =
   let doc = newDocument()
   doc.contentType = "text/html"
   doc.append(doc.newDocumentType("html"))
@@ -2617,23 +2636,24 @@ proc createHTMLDocument(implementation: DOMImplementation, title = none(string))
   #TODO set origin
   return doc
 
-proc createCDATASection(document: Document, data: string): CDATASection {.jserr, jsfunc.} =
+proc createCDATASection(document: Document, data: string): Result[CDATASection, DOMException] {.jsfunc.} =
   if not document.isxml:
-    #TODO should be DOMException
-    JS_ERR JS_TypeError, "NotSupportedError"
+    return err(newDOMException("CDATA sections are not supported in HTML",
+      "NotSupportedError"))
   if "]]>" in data:
-    #TODO should be DOMException
-    JS_ERR JS_TypeError, "InvalidCharacterError"
-  return newCDATASection(document, data)
+    return err(newDOMException("CDATA sections may not contain the string ]]>",
+      "InvalidCharacterError"))
+  return ok(newCDATASection(document, data))
 
 proc createComment*(document: Document, data: string): Comment {.jsfunc.} =
   return newComment(document, data)
 
-proc createProcessingInstruction(document: Document, target, data: string): ProcessingInstruction {.jsfunc.} =
+proc createProcessingInstruction(document: Document, target, data: string):
+    Result[ProcessingInstruction, DOMException] {.jsfunc.} =
   if not target.matchNameProduction() or "?>" in data:
-    #TODO should be DOMException
-    JS_ERR JS_TypeError, "InvalidCharacterError"
-  return newProcessingInstruction(document, target, data)
+    return err(newDOMException("Invalid data for processing instruction",
+      "InvalidCharacterError"))
+  return ok(newProcessingInstruction(document, target, data))
 
 # Forward definition hack (these are set in selectors.nim)
 var doqsa*: proc (node: Node, q: string): seq[Element]
diff --git a/src/html/htmlparser.nim b/src/html/htmlparser.nim
index f6af8709..c3df8e3d 100644
--- a/src/html/htmlparser.nim
+++ b/src/html/htmlparser.nim
@@ -14,6 +14,7 @@ import encoding/decoderstream
 import html/dom
 import html/tags
 import html/htmltokenizer
+import js/exception
 import js/javascript
 import types/url
 import utils/twtstr
@@ -239,7 +240,7 @@ proc insert(location: AdjustedInsertionLocation, node: Node) =
 proc insertForeignElement(parser: var HTML5Parser, token: Token, namespace: Namespace): Element =
   let location = parser.appropriatePlaceForInsert()
   let element = parser.createElement(token, namespace, location.inside)
-  if location.inside.preInsertionValidity(element, location.before):
+  if location.inside.preInsertionValidity(element, location.before).isOk:
     #TODO custom elements
     location.insert(element)
   parser.pushElement(element)
@@ -2239,15 +2240,16 @@ proc parseHTML*(inputStream: Stream, charsets: seq[Charset] = @[],
 proc newDOMParser*(): DOMParser {.jsctor.} =
   new(result)
 
-proc parseFromString(parser: DOMParser, str: string, t: string): Document {.jserr, jsfunc.} =
+proc parseFromString(parser: DOMParser, str: string, t: string):
+    Result[Document, JSError] {.jsfunc.} =
   case t
   of "text/html":
     let res = parseHTML(newStringStream(str))
-    return res
+    return ok(res)
   of "text/xml", "application/xml", "application/xhtml+xml", "image/svg+xml":
-    JS_ERR JS_InternalError, "XML parsing is not supported yet"
+    return err(newInternalError("XML parsing is not supported yet"))
   else:
-    JS_ERR JS_TypeError, "Invalid mime type"
+    return err(newTypeError("Invalid mime type"))
 
 proc addHTMLModule*(ctx: JSContext) =
   ctx.registerType(DOMParser)
diff --git a/src/img/path.nim b/src/img/path.nim
index 4c112f28..9180fbdd 100644
--- a/src/img/path.nim
+++ b/src/img/path.nim
@@ -4,6 +4,8 @@ import math
 
 import types/line
 import types/vector
+import js/exception
+import utils/opt
 
 type
   Path* = ref object
@@ -324,12 +326,13 @@ proc bezierCurveTo*(path: Path, cp0x, cp0y, cp1x, cp1y, x, y: float64) =
   let p = Vector2D(x: x, y: y)
   path.addBezierSegment(cp0, cp1, p)
 
-proc arcTo*(path: Path, x1, y1, x2, y2, radius: float64): bool =
+proc arcTo*(path: Path, x1, y1, x2, y2, radius: float64): Err[DOMException] =
   for v in [x1, y1, x2, y2, radius]:
     if classify(v) in {fcInf, fcNegInf, fcNan}:
-      return
+      return ok()
   if radius < 0:
-    return false
+    return err(newDOMException("Expected positive radius, but got negative",
+      "IndexSizeError"))
   path.ensureSubpath(x1, y1)
   #TODO this should be transformed by the inverse of the transformation matrix
   let v0 = path.subpaths[^1].points[^1]
@@ -353,7 +356,7 @@ proc arcTo*(path: Path, x1, y1, x2, y2, radius: float64): bool =
     )
     path.addStraightSegment(tv0)
     path.addArcSegment(origin, tv2, radius, true) #TODO always inner?
-  return true
+  return ok()
 
 func resolveEllipsePoint(o: Vector2D, angle, radiusX, radiusY,
     rotation: float64): Vector2D =
@@ -370,12 +373,13 @@ func resolveEllipsePoint(o: Vector2D, angle, radiusX, radiusY,
   return Vector2D(x: relx, y: rely).rotate(rotation) + o
 
 proc arc*(path: Path, x, y, radius, startAngle, endAngle: float64,
-    counterclockwise: bool): bool =
+    counterclockwise: bool): Err[DOMException] =
   for v in [x, y, radius, startAngle, endAngle]:
     if classify(v) in {fcInf, fcNegInf, fcNan}:
-      return
+      return ok()
   if radius < 0:
-    return false
+    return err(newDOMException("Expected positive radius, but got negative",
+      "IndexSizeError"))
   let o = Vector2D(x: x, y: y)
   var s = resolveEllipsePoint(o, startAngle, radius, radius, 0)
   var e = resolveEllipsePoint(o, endAngle, radius, radius, 0)
@@ -388,15 +392,16 @@ proc arc*(path: Path, x, y, radius, startAngle, endAngle: float64,
   else:
     path.moveTo(s)
   path.addArcSegment(o, e, radius, abs(startAngle - endAngle) < PI)
-  return true
+  return ok()
 
 proc ellipse*(path: Path, x, y, radiusX, radiusY, rotation, startAngle,
-    endAngle: float64, counterclockwise: bool): bool =
+    endAngle: float64, counterclockwise: bool): Err[DOMException] =
   for v in [x, y, radiusX, radiusY, rotation, startAngle, endAngle]:
     if classify(v) in {fcInf, fcNegInf, fcNan}:
-      return
+      return ok()
   if radiusX < 0 or radiusY < 0:
-    return false
+    return err(newDOMException("Expected positive radius, but got negative",
+      "IndexSizeError"))
   let o = Vector2D(x: x, y: y)
   var s = resolveEllipsePoint(o, startAngle, radiusX, radiusY, rotation)
   var e = resolveEllipsePoint(o, endAngle, radiusX, radiusY, rotation)
@@ -409,7 +414,7 @@ proc ellipse*(path: Path, x, y, radiusX, radiusY, rotation, startAngle,
   else:
     path.moveTo(s)
   path.addEllipseSegment(o, e, radiusX, radiusY)
-  return true
+  return ok()
 
 proc rect*(path: Path, x, y, w, h: float64) =
   for v in [x, y, w, h]:
diff --git a/src/io/request.nim b/src/io/request.nim
index 8bc2bfc8..86c8a38d 100644
--- a/src/io/request.nim
+++ b/src/io/request.nim
@@ -4,6 +4,7 @@ import strutils
 import tables
 
 import bindings/quickjs
+import js/exception
 import js/javascript
 import types/formdata
 import types/url
@@ -225,13 +226,10 @@ func createPotentialCORSRequest*(url: URL, destination: RequestDestination, cors
 #TODO resource as Request
 #TODO init as an actual dictionary
 func newRequest*(ctx: JSContext, resource: string,
-    init = none(JSValue)): Request {.jserr, jsctor.} =
-  let x = parseURL(resource)
-  if x.isNone:
-    JS_ERR JS_TypeError, resource & " is not a valid URL."
-  if x.get.username != "" or x.get.password != "":
-    JS_ERR JS_TypeError, resource & " is not a valid URL."
-  let url = x.get
+    init = none(JSValue)): Result[Request, JSError] {.jsctor.} =
+  let url = ?newURL(resource)
+  if url.username != "" or url.password != "":
+    return err(newTypeError("Input URL contains a username or password"))
   let fallbackMode = some(RequestMode.CORS) #TODO none if resource is request
   var httpMethod = HTTP_GET
   var body = opt(string)
@@ -254,7 +252,7 @@ func newRequest*(ctx: JSContext, resource: string,
     #TODO inputbody
     if (multipart.isSome or body.isSome) and
         httpMethod in {HTTP_GET, HTTP_HEAD}:
-      JS_ERR JS_TypeError, "HEAD or GET Request cannot have a body."
+      return err(newTypeError("HEAD or GET Request cannot have a body."))
     let jheaders = JS_GetPropertyStr(ctx, init, "headers")
     hl.fill(ctx, jheaders)
     credentials = fromJS[CredentialsMode](ctx, JS_GetPropertyStr(ctx, init,
@@ -263,8 +261,8 @@ func newRequest*(ctx: JSContext, resource: string,
       .get(mode)
     #TODO find a standard compatible way to implement this
     proxyUrl = fromJS[URL](ctx, JS_GetPropertyStr(ctx, init, "proxyUrl"))
-  return newRequest(url, httpMethod, hl, body, multipart, mode, credentials,
-    proxy = proxyUrl.get(nil))
+  return ok(newRequest(url, httpMethod, hl, body, multipart, mode, credentials,
+    proxy = proxyUrl.get(nil)))
 
 proc add*(headers: var Headers, k, v: string) =
   let k = k.toHeaderCase()
diff --git a/src/ips/serialize.nim b/src/ips/serialize.nim
index f7fb934e..54b44a41 100644
--- a/src/ips/serialize.nim
+++ b/src/ips/serialize.nim
@@ -167,7 +167,11 @@ proc sread*(stream: Stream, url: var URL) =
   if s == "":
     url = nil
   else:
-    url = newURL(s)
+    let x = newURL(s)
+    if x.isSome:
+      url = x.get
+    else:
+      url = nil
 
 func slen*(url: URL): int =
   if url == nil:
diff --git a/src/js/javascript.nim b/src/js/javascript.nim
index e868dde3..ced12bf7 100644
--- a/src/js/javascript.nim
+++ b/src/js/javascript.nim
@@ -109,11 +109,7 @@ type
   LegacyJSError* = object of CatchableError
 
   #TODO remove these
-  JS_SyntaxError* = object of LegacyJSError
   JS_TypeError* = object of LegacyJSError
-  JS_ReferenceError* = object of LegacyJSError
-  JS_RangeError* = object of LegacyJSError
-  JS_InternalError* = object of LegacyJSError
 
 const QuickJSErrors = [
   JS_EVAL_ERROR0,
@@ -840,13 +836,15 @@ proc toJS[T, E](ctx: JSContext, opt: Result[T, E]): JSValue =
   if opt.isSome:
     when not (T is void):
       return toJS(ctx, opt.get)
-    return JS_UNDEFINED
+    else:
+      return JS_UNDEFINED
   else:
     when not (E is void):
       let res = toJS(ctx, opt.error)
       if not JS_IsNull(res):
         return JS_Throw(ctx, res)
-    return JS_NULL
+    else:
+      return JS_NULL
 
 proc toJS(ctx: JSContext, s: seq): JSValue =
   let a = JS_NewArray(ctx)
@@ -1387,16 +1385,6 @@ proc newJSProc(gen: var JSFuncGenerator, params: openArray[NimNode], isva = true
   result = newProc(gen.newName, params, jsBody, pragmas = jsPragmas)
   gen.res = result
 
-# WARNING: for now, this only works correctly when the .jserr pragma was
-# declared on the parent function.
-# Note: this causes the entire nim function body to be inlined inside the JS
-# interface function.
-#TODO: remove this.
-macro JS_ERR*(a: typed, b: string) =
-  result = quote do:
-    block when_js:
-      raise newException(`a`, `b`)
-
 func getFuncName(fun: NimNode, jsname: string): string =
   if jsname != "":
     return jsname
@@ -1423,10 +1411,14 @@ proc addThisName(gen: var JSFuncGenerator, thisname: Option[string]) =
     gen.thisType = $gen.funcParams[gen.i][1]
     gen.newName = ident($gen.t & "_" & gen.thisType & "_" & gen.funcName)
   else:
-    if gen.returnType.get.kind == nnkRefTy:
-      gen.thisType = gen.returnType.get[0].strVal
+    let rt = gen.returnType.get
+    if rt.kind == nnkRefTy:
+      gen.thisType = rt[0].strVal
     else:
-      gen.thisType = gen.returnType.get.strVal
+      if rt.kind == nnkBracketExpr:
+        gen.thisType = rt[1].strVal
+      else:
+        gen.thisType = rt.strVal
     gen.newName = ident($gen.t & "_" & gen.funcName)
 
 func getActualMinArgs(gen: var JSFuncGenerator): int =
@@ -1462,57 +1454,6 @@ proc setupGenerator(fun: NimNode, t: BoundFunctionType,
   gen.addThisName(thisname)
   return gen
 
-# this might be pretty slow...
-#TODO ideally we wouldn't need separate functions at all. Not sure how that
-# could be achieved, maybe using options?
-proc rewriteExceptions(gen: var JSFuncGenerator, errors: var seq[string], node: NimNode) =
-  for i in countdown(node.len - 1, 0):
-    let c = node[i]
-    if c.kind == nnkCommand and c[0].eqIdent ident("JS_ERR"):
-      if gen.copied == nil:
-        gen.copied = copy(gen.original)
-      if gen.returnType.isSome:
-        node[i] = quote do:
-          zeroMem(addr result, sizeof(result))
-          return
-      else:
-        node[i] = quote do:
-          return
-      if c[1].strVal notin errors:
-        errors.add(c[1].strVal)
-    elif c.len > 0:
-      gen.rewriteExceptions(errors, c)
-
-proc rewriteExceptions(gen: var JSFuncGenerator) =
-  let ostmts = gen.original.findChild(it.kind == nnkStmtList)
-  var errors: seq[string]
-  gen.rewriteExceptions(errors, ostmts)
-  assert gen.copied != nil
-  var name: string
-  if gen.copied[0].kind == nnkIdent:
-    name = gen.copied[0].strVal
-  elif gen.copied[0].kind == nnkPostfix:
-    name = gen.copied[0][1].strVal
-  else:
-    error("No JS_ERR statement found in proc with jserr pragma.")
-  name &= "_exceptions"
-  gen.copied[0] = ident(name)
-  js_errors[name] = errors
-
-macro jserr*(fun: untyped) =
-  var gen: JSFuncGenerator
-  gen.original = fun
-  gen.rewriteExceptions()
-  var pragma = gen.original.findChild(it.kind == nnkPragma)
-  for i in 0..<pragma.len:
-    if pragma[i].eqIdent(ident("jsctor")) or pragma[i].eqIdent(ident("jsfunc")) or pragma[i].eqIdent(ident("jsget")) or pragma[i].eqIdent(ident("jsset")):
-      pragma.del(i)
-  gen.original.addPragma(quote do: used) # may be unused, but we have to keep it
-  gen.copied.addPragma(quote do: inline)
-
-  #TODO mark original as used or something
-  result = newStmtList(gen.original, gen.copied)
-
 macro jsctor*(fun: typed) =
   var gen = setupGenerator(fun, CONSTRUCTOR, thisname = none(string))
   if gen.newName.strVal in existing_funcs:
diff --git a/src/types/url.nim b/src/types/url.nim
index 9e786347..36be9e06 100644
--- a/src/types/url.nim
+++ b/src/types/url.nim
@@ -5,6 +5,7 @@ import options
 import unicode
 import math
 
+import js/exception
 import js/javascript
 import types/blob
 import utils/twtstr
@@ -962,23 +963,23 @@ proc newURL*(url: URL): URL =
     result.searchParams[] = url.searchParams[]
     result.searchParams.url = some(result)
 
-#TODO add Option wrapper
-proc newURL*(s: string, base: Option[string] = none(string)): URL {.jserr, jsctor.} =
+proc newURL*(s: string, base: Option[string] = none(string)):
+    Result[URL, JSError] {.jsctor.} =
   if base.issome:
     let baseUrl = parseURL(base.get)
-    if baseUrl.isnone:
-      JS_ERR JS_TypeError, base.get & " is not a valid URL"
+    if baseUrl.isNone:
+      return err(newTypeError(base.get & " is not a valid URL"))
     let url = parseURL(s, baseUrl)
-    if url.isnone:
-      JS_ERR JS_TypeError, s & " is not a valid URL"
-    return url.get
+    if url.isNone:
+      return err(newTypeError(s & " is not a valid URL"))
+    return ok(url.get)
   let url = parseURL(s)
-  if url.isnone:
-    JS_ERR JS_TypeError, s & " is not a valid URL"
+  if url.isNone:
+    return err(newTypeError(s & " is not a valid URL"))
   url.get.searchParams = newURLSearchParams()
   url.get.searchParams.url = url
   url.get.searchParams.initURLSearchParams(url.get.query.get(""))
-  return url.get
+  return ok(url.get)
 
 proc origin0*(url: URL): Origin =
   case url.scheme
diff --git a/src/utils/opt.nim b/src/utils/opt.nim
index 3766516b..1a86af8e 100644
--- a/src/utils/opt.nim
+++ b/src/utils/opt.nim
@@ -84,9 +84,6 @@ func error*[T, E](res: Result[T, E]): E {.inline.} = res.ex
 template valType*[T, E](res: type Result[T, E]): auto = T
 template errType*[T, E](res: type Result[T, E]): auto = E
 
-func isSameErr[T, E, F](a: type Result[T, E], b: type F): bool =
-  return E is F
-
 template `?`*[T, E](res: Result[T, E]): auto =
   let x = res # for when res is a funcall
   if x.has:
@@ -97,7 +94,7 @@ template `?`*[T, E](res: Result[T, E]): auto =
   else:
     when typeof(result) is Result[T, E]:
       return x
-    elif isSameErr(typeof(result), E):
+    elif typeof(result).errType is E:
       return err(x.error)
     else:
       return err()
diff --git a/src/xhr/formdata.nim b/src/xhr/formdata.nim
index cbb9f63e..a1832274 100644
--- a/src/xhr/formdata.nim
+++ b/src/xhr/formdata.nim
@@ -1,5 +1,6 @@
 import html/dom
 import html/tags
+import js/exception
 import js/javascript
 import types/blob
 import types/formdata
@@ -8,18 +9,22 @@ import utils/twtstr
 proc constructEntryList*(form: HTMLFormElement, submitter: Element = nil,
     encoding: string = ""): Option[seq[FormDataEntry]]
 
+proc newFormData0*(): FormData =
+  return FormData()
+
 proc newFormData*(form: HTMLFormElement = nil,
-    submitter: HTMLElement = nil): FormData {.jserr, jsctor.} =
+    submitter: HTMLElement = nil): Result[FormData, JSError] {.jsctor.} =
   let this = FormData()
   if form != nil:
     if submitter != nil:
       if not submitter.isSubmitButton():
-        JS_ERR JS_TypeError, "Submitter must be a submit button"
+        return err(newDOMException("Submitter must be a submit button",
+          "InvalidStateError"))
       if FormAssociatedElement(submitter).form != form:
-        #TODO should be DOMException
-        JS_ERR JS_TypeError, "InvalidStateError"
+        return err(newDOMException("Submitter's form owner is not form",
+          "InvalidStateError"))
     this.entries = constructEntryList(form, submitter).get(@[])
-  return this
+  return ok(this)
 
 #TODO as jsfunc
 proc append*(this: FormData, name: string, svalue: string, filename = "") =