diff --git a/crates/ra_editor/src/lib.rs b/crates/ra_editor/src/lib.rs
index 3186eaf1a86..f92181b867c 100644
--- a/crates/ra_editor/src/lib.rs
+++ b/crates/ra_editor/src/lib.rs
@@ -31,6 +31,7 @@ use ra_syntax::{
     algo::find_leaf_at_offset,
     ast::{self, AstNode, NameOwner},
     File,
+    Location,
     SyntaxKind::{self, *},
     SyntaxNodeRef, TextRange, TextUnit,
 };
@@ -100,11 +101,18 @@ pub fn highlight(file: &File) -> Vec<HighlightedRange> {
 }
 
 pub fn diagnostics(file: &File) -> Vec<Diagnostic> {
+    fn location_to_range(location: Location) -> TextRange {
+        match location {
+            Location::Offset(offset) => TextRange::offset_len(offset, 1.into()),
+            Location::Range(range) => range,
+        }
+    }
+
     file.errors()
         .into_iter()
         .map(|err| Diagnostic {
-            range: TextRange::offset_len(err.offset, 1.into()),
-            msg: "Syntax Error: ".to_string() + &err.msg,
+            range: location_to_range(err.location()),
+            msg: format!("Syntax Error: {}", err),
         })
         .collect()
 }
diff --git a/crates/ra_syntax/src/lib.rs b/crates/ra_syntax/src/lib.rs
index acc2d960333..1230028257b 100644
--- a/crates/ra_syntax/src/lib.rs
+++ b/crates/ra_syntax/src/lib.rs
@@ -54,7 +54,7 @@ pub use crate::{
     rowan::{SmolStr, TextRange, TextUnit},
     syntax_kinds::SyntaxKind,
     yellow::{
-        Direction, OwnedRoot, RefRoot, SyntaxError, SyntaxNode, SyntaxNodeRef, TreeRoot, WalkEvent,
+        Direction, OwnedRoot, RefRoot, SyntaxError, SyntaxNode, SyntaxNodeRef, TreeRoot, WalkEvent, Location,
     },
 };
 
diff --git a/crates/ra_syntax/src/parser_impl/event.rs b/crates/ra_syntax/src/parser_impl/event.rs
index 79fa21389d1..bf9c1cef095 100644
--- a/crates/ra_syntax/src/parser_impl/event.rs
+++ b/crates/ra_syntax/src/parser_impl/event.rs
@@ -13,6 +13,11 @@ use crate::{
     SmolStr,
     SyntaxKind::{self, *},
     TextRange, TextUnit,
+    yellow::syntax_error::{
+        ParseError,
+        SyntaxError,
+        SyntaxErrorKind,
+    },
 };
 use std::mem;
 
@@ -75,7 +80,7 @@ pub(crate) enum Event {
     },
 
     Error {
-        msg: String,
+        msg: ParseError,
     },
 }
 
@@ -157,7 +162,10 @@ impl<'a, S: Sink> EventProcessor<'a, S> {
                         .sum::<TextUnit>();
                     self.leaf(kind, len, n_raw_tokens);
                 }
-                Event::Error { msg } => self.sink.error(msg, self.text_pos),
+                Event::Error { msg } => self.sink.error(SyntaxError::new(
+                    SyntaxErrorKind::ParseError(msg),
+                    self.text_pos,
+                )),
             }
         }
         self.sink
diff --git a/crates/ra_syntax/src/parser_impl/mod.rs b/crates/ra_syntax/src/parser_impl/mod.rs
index 2b026d61e4c..cb6e370ac51 100644
--- a/crates/ra_syntax/src/parser_impl/mod.rs
+++ b/crates/ra_syntax/src/parser_impl/mod.rs
@@ -10,7 +10,11 @@ use crate::{
         event::{Event, EventProcessor},
         input::{InputPosition, ParserInput},
     },
-    SmolStr, TextUnit,
+    SmolStr,
+    yellow::syntax_error::{
+        ParseError,
+        SyntaxError,
+    },
 };
 
 use crate::SyntaxKind::{self, EOF, TOMBSTONE};
@@ -21,7 +25,7 @@ pub(crate) trait Sink {
     fn leaf(&mut self, kind: SyntaxKind, text: SmolStr);
     fn start_internal(&mut self, kind: SyntaxKind);
     fn finish_internal(&mut self);
-    fn error(&mut self, message: String, offset: TextUnit);
+    fn error(&mut self, error: SyntaxError);
     fn finish(self) -> Self::Tree;
 }
 
@@ -144,7 +148,9 @@ impl<'t> ParserImpl<'t> {
     }
 
     pub(super) fn error(&mut self, msg: String) {
-        self.event(Event::Error { msg })
+        self.event(Event::Error {
+            msg: ParseError(msg),
+        })
     }
 
     pub(super) fn complete(&mut self, pos: u32, kind: SyntaxKind) {
diff --git a/crates/ra_syntax/src/reparsing.rs b/crates/ra_syntax/src/reparsing.rs
index b3b51b3e486..3c4ea5c22b7 100644
--- a/crates/ra_syntax/src/reparsing.rs
+++ b/crates/ra_syntax/src/reparsing.rs
@@ -165,20 +165,14 @@ fn merge_errors(
 ) -> Vec<SyntaxError> {
     let mut res = Vec::new();
     for e in old_errors {
-        if e.offset <= old_node.range().start() {
+        if e.offset() <= old_node.range().start() {
             res.push(e)
-        } else if e.offset >= old_node.range().end() {
-            res.push(SyntaxError {
-                msg: e.msg,
-                offset: e.offset + TextUnit::of_str(&edit.insert) - edit.delete.len(),
-            })
+        } else if e.offset() >= old_node.range().end() {
+            res.push(e.add_offset(TextUnit::of_str(&edit.insert) - edit.delete.len()));
         }
     }
     for e in new_errors {
-        res.push(SyntaxError {
-            msg: e.msg,
-            offset: e.offset + old_node.range().start(),
-        })
+        res.push(e.add_offset(old_node.range().start()));
     }
     res
 }
diff --git a/crates/ra_syntax/src/string_lexing/mod.rs b/crates/ra_syntax/src/string_lexing/mod.rs
index 6b52c62c381..f0812ff289d 100644
--- a/crates/ra_syntax/src/string_lexing/mod.rs
+++ b/crates/ra_syntax/src/string_lexing/mod.rs
@@ -100,10 +100,6 @@ impl<'a> Parser<'a> {
     // Char parsing methods
 
     fn parse_unicode_escape(&mut self, start: TextUnit) -> CharComponent {
-        // Note: validation of UnicodeEscape will be done elsewhere:
-        // * Only hex digits or underscores allowed
-        // * Max 6 chars
-        // * Within allowed range (must be at most 10FFFF)
         match self.peek() {
             Some('{') => {
                 self.advance();
@@ -127,9 +123,6 @@ impl<'a> Parser<'a> {
     }
 
     fn parse_ascii_code_escape(&mut self, start: TextUnit) -> CharComponent {
-        // Note: validation of AsciiCodeEscape will be done elsewhere:
-        // * First digit is octal
-        // * Second digit is hex
         let code_start = self.get_pos();
         while let Some(next) = self.peek() {
             if next == '\'' || (self.get_pos() - code_start == 2.into()) {
@@ -144,9 +137,6 @@ impl<'a> Parser<'a> {
     }
 
     fn parse_escape(&mut self, start: TextUnit) -> CharComponent {
-        // Note: validation of AsciiEscape will be done elsewhere:
-        // * The escape sequence is non-empty
-        // * The escape sequence is valid
         if self.peek().is_none() {
             return CharComponent::new(TextRange::from_to(start, start), AsciiEscape);
         }
diff --git a/crates/ra_syntax/src/utils.rs b/crates/ra_syntax/src/utils.rs
index 00f00139a0d..288d7edd4e8 100644
--- a/crates/ra_syntax/src/utils.rs
+++ b/crates/ra_syntax/src/utils.rs
@@ -4,7 +4,7 @@ use std::fmt::Write;
 /// Parse a file and create a string representation of the resulting parse tree.
 pub fn dump_tree(syntax: SyntaxNodeRef) -> String {
     let mut errors: Vec<_> = syntax.root_data().to_vec();
-    errors.sort_by_key(|e| e.offset);
+    errors.sort_by_key(|e| e.offset());
     let mut err_pos = 0;
     let mut level = 0;
     let mut buf = String::new();
@@ -23,9 +23,9 @@ pub fn dump_tree(syntax: SyntaxNodeRef) -> String {
                 writeln!(buf, "{:?}", node).unwrap();
                 if node.first_child().is_none() {
                     let off = node.range().end();
-                    while err_pos < errors.len() && errors[err_pos].offset <= off {
+                    while err_pos < errors.len() && errors[err_pos].offset() <= off {
                         indent!();
-                        writeln!(buf, "err: `{}`", errors[err_pos].msg).unwrap();
+                        writeln!(buf, "err: `{}`", errors[err_pos]).unwrap();
                         err_pos += 1;
                     }
                 }
@@ -37,7 +37,7 @@ pub fn dump_tree(syntax: SyntaxNodeRef) -> String {
 
     assert_eq!(level, 0);
     for err in errors[err_pos..].iter() {
-        writeln!(buf, "err: `{}`", err.msg).unwrap();
+        writeln!(buf, "err: `{}`", err).unwrap();
     }
 
     buf
diff --git a/crates/ra_syntax/src/validation.rs b/crates/ra_syntax/src/validation.rs
index 03d98eff490..009f5052f92 100644
--- a/crates/ra_syntax/src/validation.rs
+++ b/crates/ra_syntax/src/validation.rs
@@ -1,40 +1,78 @@
 use crate::{
+    algo::visit::{visitor_ctx, VisitorCtx},
     ast::{self, AstNode},
     File,
-    string_lexing,
+    string_lexing::{self, CharComponentKind},
     yellow::{
         SyntaxError,
+        SyntaxErrorKind::*,
     },
 };
 
 pub(crate) fn validate(file: &File) -> Vec<SyntaxError> {
     let mut errors = Vec::new();
-    for d in file.root.borrowed().descendants() {
-        if let Some(c) = ast::Char::cast(d) {
-            let components = &mut string_lexing::parse_char_literal(c.text());
-            let len = components.count();
-
-            if !components.has_closing_quote {
-                errors.push(SyntaxError {
-                    msg: "Unclosed char literal".to_string(),
-                    offset: d.range().start(),
-                });
-            }
-
-            if len == 0 {
-                errors.push(SyntaxError {
-                    msg: "Empty char literal".to_string(),
-                    offset: d.range().start(),
-                });
-            }
-
-            if len > 1 {
-                errors.push(SyntaxError {
-                    msg: "Character literal should be only one character long".to_string(),
-                    offset: d.range().start(),
-                });
-            }
-        }
+    for node in file.root.borrowed().descendants() {
+        let _ = visitor_ctx(&mut errors)
+            .visit::<ast::Char, _>(validate_char)
+            .accept(node);
     }
     errors
 }
+
+fn validate_char(node: ast::Char, errors: &mut Vec<SyntaxError>) {
+    let mut components = string_lexing::parse_char_literal(node.text());
+    let mut len = 0;
+    for component in &mut components {
+        len += 1;
+
+        // Validate escapes
+        let text = &node.text()[component.range];
+        let range = component.range + node.syntax().range().start();
+        use self::CharComponentKind::*;
+        match component.kind {
+            AsciiEscape => {
+                if text.len() == 1 {
+                    // Escape sequence consists only of leading `\`
+                    errors.push(SyntaxError::new(EmptyAsciiEscape, range));
+                } else {
+                    let escape_code = text.chars().skip(1).next().unwrap();
+                    if !is_ascii_escape(escape_code) {
+                        errors.push(SyntaxError::new(InvalidAsciiEscape, range));
+                    }
+                }
+            }
+            AsciiCodeEscape => {
+                // TODO:
+                // * First digit is octal
+                // * Second digit is hex
+            }
+            UnicodeEscape => {
+                // TODO:
+                // * Only hex digits or underscores allowed
+                // * Max 6 chars
+                // * Within allowed range (must be at most 10FFFF)
+            }
+            // Code points are always valid
+            CodePoint => (),
+        }
+    }
+
+    if !components.has_closing_quote {
+        errors.push(SyntaxError::new(UnclosedChar, node.syntax().range()));
+    }
+
+    if len == 0 {
+        errors.push(SyntaxError::new(EmptyChar, node.syntax().range()));
+    }
+
+    if len > 1 {
+        errors.push(SyntaxError::new(LongChar, node.syntax().range()));
+    }
+}
+
+fn is_ascii_escape(code: char) -> bool {
+    match code {
+        '\'' | '"' | 'n' | 'r' | 't' | '0' => true,
+        _ => false,
+    }
+}
diff --git a/crates/ra_syntax/src/yellow/builder.rs b/crates/ra_syntax/src/yellow/builder.rs
index d6405340993..9fcebfb93b9 100644
--- a/crates/ra_syntax/src/yellow/builder.rs
+++ b/crates/ra_syntax/src/yellow/builder.rs
@@ -1,7 +1,7 @@
 use crate::{
     parser_impl::Sink,
     yellow::{GreenNode, RaTypes, SyntaxError},
-    SmolStr, SyntaxKind, TextUnit,
+    SmolStr, SyntaxKind,
 };
 use rowan::GreenNodeBuilder;
 
@@ -34,11 +34,7 @@ impl Sink for GreenBuilder {
         self.inner.finish_internal();
     }
 
-    fn error(&mut self, message: String, offset: TextUnit) {
-        let error = SyntaxError {
-            msg: message,
-            offset,
-        };
+    fn error(&mut self, error: SyntaxError) {
         self.errors.push(error)
     }
 
diff --git a/crates/ra_syntax/src/yellow/mod.rs b/crates/ra_syntax/src/yellow/mod.rs
index 65091721423..6da9486489d 100644
--- a/crates/ra_syntax/src/yellow/mod.rs
+++ b/crates/ra_syntax/src/yellow/mod.rs
@@ -1,8 +1,9 @@
 mod builder;
+pub mod syntax_error;
 mod syntax_text;
 
 use self::syntax_text::SyntaxText;
-use crate::{SmolStr, SyntaxKind, TextRange, TextUnit};
+use crate::{SmolStr, SyntaxKind, TextRange};
 use rowan::Types;
 use std::{
     fmt,
@@ -10,6 +11,7 @@ use std::{
 };
 
 pub(crate) use self::builder::GreenBuilder;
+pub use self::syntax_error::{SyntaxError, SyntaxErrorKind, Location};
 pub use rowan::{TreeRoot, WalkEvent};
 
 #[derive(Debug, Clone, Copy)]
@@ -24,12 +26,6 @@ pub type RefRoot<'a> = ::rowan::RefRoot<'a, RaTypes>;
 
 pub type GreenNode = ::rowan::GreenNode<RaTypes>;
 
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
-pub struct SyntaxError {
-    pub msg: String,
-    pub offset: TextUnit,
-}
-
 #[derive(Clone, Copy)]
 pub struct SyntaxNode<R: TreeRoot<RaTypes> = OwnedRoot>(::rowan::SyntaxNode<RaTypes, R>);
 pub type SyntaxNodeRef<'a> = SyntaxNode<RefRoot<'a>>;
diff --git a/crates/ra_syntax/src/yellow/syntax_error.rs b/crates/ra_syntax/src/yellow/syntax_error.rs
new file mode 100644
index 00000000000..f3df6bc1531
--- /dev/null
+++ b/crates/ra_syntax/src/yellow/syntax_error.rs
@@ -0,0 +1,89 @@
+use std::fmt;
+
+use crate::{TextRange, TextUnit};
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct SyntaxError {
+    kind: SyntaxErrorKind,
+    location: Location,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub enum Location {
+    Offset(TextUnit),
+    Range(TextRange),
+}
+
+impl Into<Location> for TextUnit {
+    fn into(self) -> Location {
+        Location::Offset(self)
+    }
+}
+
+impl Into<Location> for TextRange {
+    fn into(self) -> Location {
+        Location::Range(self)
+    }
+}
+
+impl SyntaxError {
+    pub fn new<L: Into<Location>>(kind: SyntaxErrorKind, loc: L) -> SyntaxError {
+        SyntaxError {
+            kind,
+            location: loc.into(),
+        }
+    }
+
+    pub fn location(&self) -> Location {
+        self.location.clone()
+    }
+
+    pub fn offset(&self) -> TextUnit {
+        match self.location {
+            Location::Offset(offset) => offset,
+            Location::Range(range) => range.start(),
+        }
+    }
+
+    pub fn add_offset(mut self, plus_offset: TextUnit) -> SyntaxError {
+        self.location = match self.location {
+            Location::Range(range) => Location::Range(range + plus_offset),
+            Location::Offset(offset) => Location::Offset(offset + plus_offset),
+        };
+
+        self
+    }
+}
+
+impl fmt::Display for SyntaxError {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        self.kind.fmt(f)
+    }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub enum SyntaxErrorKind {
+    ParseError(ParseError),
+    EmptyChar,
+    UnclosedChar,
+    LongChar,
+    EmptyAsciiEscape,
+    InvalidAsciiEscape,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct ParseError(pub String);
+
+impl fmt::Display for SyntaxErrorKind {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        use self::SyntaxErrorKind::*;
+        match self {
+            EmptyAsciiEscape => write!(f, "Empty escape sequence"),
+            InvalidAsciiEscape => write!(f, "Invalid escape sequence"),
+            EmptyChar => write!(f, "Empty char literal"),
+            UnclosedChar => write!(f, "Unclosed char literal"),
+            LongChar => write!(f, "Char literal should be one character long"),
+            ParseError(msg) => write!(f, "{}", msg.0),
+        }
+    }
+}